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
This commit is contained in:
2026-06-13 20:52:24 +08:00
commit 1e39b6da88
49 changed files with 7172 additions and 0 deletions
+890
View File
@@ -0,0 +1,890 @@
"""CRUD 命令:download / sync / info / delete / create。"""
from __future__ import annotations
import argparse
import logging
import os
import xml.etree.ElementTree as ET
from sapcli.client import ADTClient
from sapcli.exceptions import (
ConfigError,
CreateError,
DeleteError,
InvalidNameError,
LockError,
ObjectAlreadyExistsError,
ObjectNotFoundError,
SapCliError,
)
from sapcli.manifest import Manifest, ManifestEntry
from sapcli.types import get_type_config, parse_object_name
DDIC_TYPES = {"domain", "dataelement", "table", "structure", "tabletype"}
logger = logging.getLogger("sapcli.commands.crud")
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
LOG_DIR = os.path.join(_PROJECT_ROOT, "log")
LOG_FILE = os.path.join(LOG_DIR, "adt_tools.log")
DEFAULT_TEMPLATES: dict[str, str] = {
"report": 'REPORT {name}.\nWRITE: / \'Hello from {name}\'.\n',
"class": (
'CLASS {name} DEFINITION\n'
' PUBLIC\n'
' FINAL\n'
' CREATE PUBLIC.\n'
' PUBLIC SECTION.\n'
' METHODS: hello.\n'
'ENDCLASS.\n'
'CLASS {name} IMPLEMENTATION.\n'
' METHOD hello.\n'
' ENDMETHOD.\n'
'ENDCLASS.\n'
),
"function": (
'FUNCTION {name}\n'
' EXPORTING\n'
' VALUE(EV_RESULT) TYPE STRING.\n'
' ev_result = \'hello\'.\n'
'ENDFUNCTION.\n'
),
"interface": (
'INTERFACE {name}\n'
' PUBLIC.\n'
' METHODS: hello.\n'
'ENDINTERFACE.\n'
),
}
def print_source_preview(source: str, max_lines: int = 20) -> None:
"""打印源代码预览。"""
lines = source.splitlines()
print(f" ┌─── 源代码 (前 {max_lines} 行) ──────────────────────")
for i, line in enumerate(lines[:max_lines], 1):
print(f"{i:4d} | {line}")
if len(lines) > max_lines:
print(f" │ ... 省略剩余 {len(lines) - max_lines} 行 ...")
print(f" └────────────────────────────────────────────")
def cmd_download(args: argparse.Namespace, client: ADTClient) -> None:
"""下载 SAP 对象源代码到本地文件。"""
obj_type: str = args.type
name: str = args.name
save_dir: str = args.path
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
print(f"\n{type_label}没有源代码,不支持 download 操作")
print(f" functiongroup 是函数模块的容器,不包含可编辑的源代码文件")
raise InvalidNameError(f"{type_label}没有源代码,不支持 download 操作")
print("=" * 60)
print(" SAP ADT 源代码下载")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f" 保存路径: {save_dir}")
print(f"\n → 检查对象是否存在...")
if not client.object_exists(parsed.obj_uri):
print(f" ✗ 对象不存在: {parsed.display_name}")
print(f" 请确认名称和类型是否正确")
raise ObjectNotFoundError(parsed.display_name, obj_type)
print(" ✓ 对象存在")
print(f"\n → 正在下载源代码...")
source = client.get_source(parsed.src_uri)
source = source.replace("\r\n", "\n").replace("\r", "\n")
line_count = len(source.splitlines())
print(f" ✓ 源代码下载成功! {len(source)} 字符, {line_count}")
if not os.path.isdir(save_dir):
os.makedirs(save_dir, exist_ok=True)
filename = f"{parsed.file_base}.abap"
filepath = os.path.join(save_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(source)
print(f"\n ✓ 文件已保存: {filepath}")
print()
print_source_preview(source)
print(f"\n ✓ 下载完成!")
print(f" 文件: {filepath}")
print(f" 大小: {len(source)} 字符, {line_count}")
def cmd_sync(args: argparse.Namespace, client: ADTClient) -> None:
"""同步单个对象源代码到 SAP 并激活。"""
obj_type: str = args.type
name: str = args.name
file_path: str = args.path
# 向后兼容:检测项目根目录(从文件路径向上查找 manifest.json
if not hasattr(args, "project_path") or not args.project_path:
parent = os.path.dirname(os.path.abspath(file_path))
if os.path.isfile(os.path.join(parent, "manifest.json")):
args.project_path = parent
else:
args.project_path = None
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
print(f"\n{type_label}没有源代码,不支持 sync 操作")
print(f" functiongroup 是函数模块的容器,不包含可编辑的源代码文件")
raise InvalidNameError(f"{type_label}没有源代码,不支持 sync 操作")
if not os.path.isfile(file_path):
print(f"\n ✗ 文件不存在: {file_path}")
raise ConfigError(f"文件不存在: {file_path}")
with open(file_path, "r", encoding="utf-8") as f:
source = f.read()
print("=" * 60)
print(" SAP ADT 源代码同步激活")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f" 本地文件: {file_path}")
print(f" 源代码: {len(source)} 字符, {len(source.splitlines())}")
print(f" 流程: 检查/创建 → 写入 → 语法检查 → 激活")
corr_nr_arg = getattr(args, "corr_nr", None)
success, error_msg, actual_corr_nr = _sync_single(
name, obj_type, file_path, client, corr_nr=corr_nr_arg,
)
if success:
# 更新清单(如果适用)
_update_manifest_after_sync(args, name, obj_type, file_path, actual_corr_nr)
return
# 失败时也更新清单
_update_manifest_after_sync(args, name, obj_type, file_path, actual_corr_nr, failed=True)
raise SapCliError(error_msg or "同步失败")
def _sync_single(
name: str,
obj_type: str,
file_path: str,
client: ADTClient,
corr_nr: str | None = None,
quiet: bool = False,
) -> tuple[bool, str | None, str | None]:
"""单个对象的同步核心逻辑。
Args:
name: 对象名称(function 类型含 /)
obj_type: 对象类型
file_path: 本地源代码文件路径
client: ADT 客户端
corr_nr: 传输请求号(可选)
quiet: 是否静默模式(批量同步时减少输出)
Returns:
(success, error_msg, actual_corr_nr)
"""
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
if parsed.src_uri is None:
return False, f"{type_label}没有源代码,不支持 sync 操作", None
if not os.path.isfile(file_path):
return False, f"文件不存在: {file_path}", None
with open(file_path, "r", encoding="utf-8") as f:
source = f.read()
if not quiet:
print(f"\n → 检查对象是否存在...")
if not client.object_exists(parsed.obj_uri):
if not quiet:
print(f" ℹ 对象不存在,自动创建空对象...")
try:
client.create_object(obj_type, name, name, source=None)
if not quiet:
print(f" ✓ 空对象创建成功,进入同步流程")
except Exception as e:
return False, f"创建失败: {e}", None
else:
if not quiet:
print(" ✓ 对象存在")
# ── Step 1: 锁定 → 写入 → 解锁 ──
if not quiet:
print("\n ── 锁定 → 写入 → 解锁 ──")
try:
if corr_nr:
lock_handle, _ = client.lock(parsed.obj_uri, corr_nr)
if not quiet:
print(f" ✓ 锁定成功(传输请求: {corr_nr}")
else:
try:
lock_handle, detected_corr_nr = client.lock(parsed.obj_uri)
if detected_corr_nr:
corr_nr = detected_corr_nr
if not quiet:
print(f" ✓ 锁定成功(对象已绑定传输请求: {corr_nr}")
else:
if not quiet:
print(f" ✓ 锁定成功(本地对象,无需传输请求)")
except LockError:
if not quiet:
print(" 对象需要传输请求号")
corr_nr = _select_transport_request(client)
if corr_nr:
lock_handle, _ = client.lock(parsed.obj_uri, corr_nr)
if not quiet:
print(f" ✓ 锁定成功(传输请求: {corr_nr}")
else:
return False, "锁定失败: 无法获取传输请求号", None
except LockError as e:
return False, str(e), None
try:
client.set_source(parsed.src_uri, source, lock_handle, corr_nr)
if not quiet:
print(f" ✓ 源代码写入成功")
finally:
client.unlock(parsed.obj_uri, lock_handle)
if not quiet:
print(f" ✓ 解锁成功")
# ── Step 2: 语法检查 ──
if not quiet:
print("\n ── 语法检查 ──")
try:
check_ok, check_msgs = client.syntax_check(name, parsed.obj_uri)
except Exception as e:
if not quiet:
print(f" ℹ 语法检查异常(跳过,直接激活): {e}")
check_ok = True
check_msgs = []
if not check_ok:
errors = [m for m in check_msgs if m["type"] == "E"]
warnings = [m for m in check_msgs if m["type"] == "W"]
if not quiet:
print(f" ✗ 语法检查未通过! {len(errors)} 个错误, {len(warnings)} 个警告")
for e in errors:
print(f" [错误] 行 {e['line']}: {e['text']}")
for w in warnings:
print(f" [警告] 行 {w['line']}: {w['text']}")
error_summary = "; ".join(f"{e['line']}: {e['text']}" for e in errors)
return False, f"语法检查未通过: {error_summary}", corr_nr
if not quiet:
print(" ✓ 语法检查通过")
# ── Step 3: 激活 ──
if not quiet:
print("\n ── 激活对象 ──")
success, messages = client.activate(name, parsed.obj_uri, corr_nr)
if success:
if not quiet:
print(f" ✓ 激活成功")
print(f"\n {'=' * 60}")
print(f"{parsed.display_name} 已成功同步并激活")
print(f" 文件: {file_path}")
print(f" 源代码: {len(source)} 字符, {len(source.splitlines())}")
print(f" 日志: {LOG_FILE}")
print(f" {'=' * 60}")
return True, None, corr_nr
errors = [m for m in messages if m["type"] == "E"]
if not quiet:
warnings = [m for m in messages if m["type"] == "W"]
print(f" ✗ 激活失败! {len(errors)} 个错误, {len(warnings)} 个警告")
for e in errors:
print(f" [错误] 行 {e['line']}: {e['text']}")
for w in warnings:
print(f" [警告] 行 {w['line']}: {w['text']}")
error_summary = "; ".join(f"{e['line']}: {e['text']}" for e in errors)
return False, f"激活失败: {error_summary}", corr_nr
def _update_manifest_after_sync(
args: argparse.Namespace,
name: str,
obj_type: str,
file_path: str,
corr_nr: str | None,
failed: bool = False,
) -> None:
"""sync 成功/失败后尝试更新清单(如果适用)。"""
from datetime import datetime, timezone
project_path = getattr(args, "project_path", None)
if not project_path:
return
manifest_path = os.path.join(project_path, "manifest.json")
if not os.path.isfile(manifest_path):
return
try:
manifest = Manifest.load(project_path)
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
# 确定相对文件路径
try:
rel_file = os.path.relpath(file_path, project_path).replace("\\", "/")
except ValueError:
rel_file = file_path
entry = manifest.get(name)
if entry:
entry.corr_nr = corr_nr
entry.system_status = "active" if not failed else entry.system_status
entry.last_sync = now
entry.last_sync_result = "failed" if failed else "success"
else:
manifest.upsert(ManifestEntry(
name=name,
type=obj_type,
file=rel_file,
system_status="active" if not failed else "inactive",
corr_nr=corr_nr,
depends_on=[],
last_sync=now,
last_sync_result="failed" if failed else "success",
))
manifest.save()
except Exception as e:
logger.warning("更新清单失败: %s", e)
def cmd_info(args: argparse.Namespace, client: ADTClient) -> None:
"""查询对象元数据信息。"""
obj_type: str = args.type
name: str = args.name
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
INFO_ACCEPT: dict[str, str] = {
"report": "application/vnd.sap.adt.programs.programs.v2+xml",
"class": "application/vnd.sap.adt.oo.classes.v2+xml",
"interface": "application/vnd.sap.adt.oo.interfaces.v2+xml",
"function": "application/vnd.sap.adt.functions.fmodules.v2+xml",
"functiongroup": "application/vnd.sap.adt.functions.groups.v2+xml",
"domain": "*/*",
"dataelement": "*/*",
"table": "*/*",
"tabletype": "*/*",
}
print("=" * 60)
print(" SAP ADT 对象信息查询")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f"\n → 正在查询对象信息...")
accept = INFO_ACCEPT.get(obj_type, "application/xml")
url = client.host + parsed.obj_uri
hdrs = client._headers("application/xml")
hdrs["Accept"] = accept
logger.info("INFO: GET %s", url)
resp = client.session.get(url, headers=hdrs)
logger.info("INFO RESPONSE: HTTP %s", resp.status_code)
if resp.status_code == 404:
print(f" ✗ 对象不存在: {parsed.display_name}")
raise ObjectNotFoundError(parsed.display_name, obj_type)
if resp.status_code == 406:
hdrs["Accept"] = "application/xml"
resp = client.session.get(url, headers=hdrs)
if resp.status_code != 200:
print(f" ✗ 查询失败: HTTP {resp.status_code}")
print(f" {resp.text[:200]}")
raise SapCliError(f"查询失败: HTTP {resp.status_code}")
root = ET.fromstring(resp.content)
ns = {"adtcore": "http://www.sap.com/adt/core"}
info_name = root.attrib.get(f"{{{ns['adtcore']}}}name", "")
info_type = root.attrib.get(f"{{{ns['adtcore']}}}type", "")
info_desc = root.attrib.get(f"{{{ns['adtcore']}}}description", "")
info_version = root.attrib.get(f"{{{ns['adtcore']}}}version", "")
info_changed_at = root.attrib.get(f"{{{ns['adtcore']}}}changedAt", "")
info_changed_by = root.attrib.get(f"{{{ns['adtcore']}}}changedBy", "")
info_created_by = root.attrib.get(f"{{{ns['adtcore']}}}createdBy", "")
info_responsible = root.attrib.get(f"{{{ns['adtcore']}}}responsible", "")
info_language = root.attrib.get(f"{{{ns['adtcore']}}}masterLanguage", "")
if info_version == "active":
status_icon = "✓ 已激活"
elif info_version == "inactive":
status_icon = "⚠️ 未激活"
else:
status_icon = f"{info_version}"
print(f" ✓ 查询成功\n")
print(f" {'' * 50}")
print(f" 名称: {info_name}")
print(f" 类型: {info_type}")
print(f" 描述: {info_desc}")
print(f" 状态: {status_icon}")
print(f" 负责人: {info_responsible}")
print(f" 语言: {info_language}")
print(f" 创建者: {info_created_by}")
print(f" 修改者: {info_changed_by}")
print(f" 修改时间: {info_changed_at}")
print(f" URI: {parsed.obj_uri}")
print(f" {'' * 50}")
expected_name = parsed.display_name.upper().split("/")[-1]
returned_name = info_name.upper()
if returned_name != expected_name:
print(f"\n ✗ 名称不匹配! 请求: {expected_name}, 返回: {returned_name}")
print(f" 可能查询到了错误的对象")
raise SapCliError(f"名称不匹配! 请求: {expected_name}, 返回: {returned_name}")
def cmd_delete(args: argparse.Namespace, client: ADTClient) -> None:
"""从 SAP 系统删除对象。"""
obj_type: str = args.type
name: str = args.name
parsed = parse_object_name(name, obj_type)
type_label = get_type_config(obj_type).label
print("=" * 60)
print(" SAP ADT 源代码删除")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {parsed.display_name}")
print(f"\n → 检查对象是否存在...")
if not client.object_exists(parsed.obj_uri):
print(f" ✗ 对象不存在: {parsed.display_name}")
print(f" 请确认名称和类型是否正确")
raise ObjectNotFoundError(parsed.display_name, obj_type)
print(" ✓ 对象存在")
print(f"\n ⚠️ 即将从 SAP 系统中删除: {parsed.display_name}")
print(f" 类型: {type_label}")
print(f" URI: {parsed.obj_uri}")
confirm = input("\n 确认删除? (输入 yes 确认): ").strip()
if confirm.lower() != "yes":
print(" 已取消删除操作")
return
corr_nr = client.get_transport_request()
if corr_nr:
print(f" ✓ 传输请求: {corr_nr}")
else:
print(" ℹ 未找到可修改的传输请求,将尝试无传输号删除")
print(f"\n → 正在删除对象...")
success, error = client.delete_object(parsed.obj_uri, corr_nr)
if success:
print(f" ✓ 删除成功!")
print(f"\n{parsed.display_name} 已从 SAP 系统中删除")
# 更新清单(如果适用)
_update_manifest_after_delete(args, name)
else:
print(f" ✗ 删除失败: {error}")
print(f"\n 可能原因:")
print(f" - 对象被其他用户锁定")
print(f" - 缺少删除权限")
print(f" - 需要传输请求号")
raise DeleteError(f"删除失败: {error}")
def _update_manifest_after_delete(
args: argparse.Namespace,
name: str,
) -> None:
"""delete 成功后尝试从清单移除(如果适用)。"""
project_path = getattr(args, "project_path", None)
if not project_path:
return
manifest_path = os.path.join(project_path, "manifest.json")
if not os.path.isfile(manifest_path):
return
try:
manifest = Manifest.load(project_path)
if manifest.remove(name):
manifest.save()
logger.info("清单已更新: 移除 %s", name)
except Exception as e:
logger.warning("更新清单失败: %s", e)
def _select_transport_request(client: ADTClient) -> str | None:
"""交互式传输请求选择:列出已有请求或新建。
Returns:
选中的传输请求编号,或 None 表示无传输号创建。
"""
print(f"\n → 查询可用的传输请求...")
try:
requests_list = client.list_transport_requests()
except Exception as e:
logger.warning("查询传输请求失败: %s", e)
print(f" ℹ 查询传输请求失败,将尝试无传输号创建")
return None
if requests_list:
print(f" 找到 {len(requests_list)} 个可修改的传输请求:\n")
for i, req in enumerate(requests_list, 1):
desc = req.get("description", "")
owner = req.get("owner", "")
print(f" {i}. {req['number']} {desc} (所有者: {owner})")
print(f" {len(requests_list) + 1}. 新建传输请求")
print(f" 0. 不使用传输请求(本地对象)")
print()
while True:
choice = input(" 请选择 [0-{}]: ".format(len(requests_list) + 1)).strip()
if not choice:
continue
try:
idx = int(choice)
except ValueError:
print(" ✗ 请输入数字")
continue
if idx == 0:
print(" ℹ 将尝试无传输号创建(本地对象 $TMP)")
return None
elif 1 <= idx <= len(requests_list):
selected = requests_list[idx - 1]["number"]
print(f" ✓ 已选择传输请求: {selected}")
return selected
elif idx == len(requests_list) + 1:
# 新建传输请求
tr_desc = input(" 请输入新传输请求描述: ").strip()
if not tr_desc:
print(" ✗ 描述不能为空,请重新选择")
continue
try:
new_nr = client.create_transport_request(tr_desc)
if new_nr:
print(f" ✓ 传输请求已创建: {new_nr}")
return new_nr
else:
print(" ✗ 创建传输请求失败(未返回编号),将尝试无传输号创建")
return None
except Exception as e:
print(f" ✗ 创建传输请求失败: {e}")
return None
else:
print(f" ✗ 请输入 0-{len(requests_list) + 1} 之间的数字")
else:
print(" 未找到可修改的传输请求")
print()
choice = input(" 是否新建传输请求? (y/n): ").strip().lower()
if choice in ("y", "yes"):
tr_desc = input(" 请输入新传输请求描述: ").strip()
if tr_desc:
try:
new_nr = client.create_transport_request(tr_desc)
if new_nr:
print(f" ✓ 传输请求已创建: {new_nr}")
return new_nr
except Exception as e:
print(f" ✗ 创建传输请求失败: {e}")
else:
print(" ✗ 描述不能为空")
print(" 将尝试无传输号创建")
return None
def cmd_create(args: argparse.Namespace, client: ADTClient) -> None:
"""在 SAP 系统创建开发对象。"""
obj_type: str = args.type
name: str = args.name
description: str = getattr(args, "description", None) or name
source_file: str | None = getattr(args, "source", None)
definition_file: str | None = getattr(args, "definition", None)
type_label = get_type_config(obj_type).label
if obj_type == "function":
if "/" not in name:
print(f" ✗ function 类型需要'函数组名/函数模块名' 格式,例如: ZGROUP/Z_MY_FUNC")
raise InvalidNameError(
"function 类型需要'函数组名/函数模块名' 格式,例如: ZGROUP/Z_MY_FUNC"
)
display_name = name
print("=" * 60)
print(" SAP ADT 创建开发对象")
print("=" * 60)
print(f" 对象类型: {type_label}")
print(f" 对象名称: {display_name}")
print(f" 描述: {description}")
print(f"\n → 检查对象是否存在...")
if obj_type != "functiongroup":
parsed = parse_object_name(name, obj_type)
if client.object_exists(parsed.obj_uri):
print(f" ✗ 对象已存在: {display_name}")
raise ObjectAlreadyExistsError(display_name, type_label)
else:
if client.function_group_exists(name):
print(f" ✗ 函数组已存在: {name}")
raise ObjectAlreadyExistsError(name, type_label)
print(" ✓ 对象不存在,可以创建")
# ── 传输请求选择 ──
corr_nr = getattr(args, "corr_nr", None)
if corr_nr:
# 非交互模式:用户通过 --corr_nr 指定
print(f" ✓ 传输请求(指定): {corr_nr}")
else:
corr_nr = _select_transport_request(client)
if obj_type == "function":
group_name = name.split("/", 1)[0]
if not client.function_group_exists(group_name):
print(f"\n → 函数组 {group_name} 不存在,自动创建...")
try:
client.create_function_group(group_name, corr_nr=corr_nr)
print(f" ✓ 函数组 {group_name} 创建成功")
except Exception as e:
print(f" ✗ 函数组创建失败: {e}")
raise CreateError(f"函数组创建失败: {e}") from e
else:
print(f" ✓ 函数组 {group_name} 已存在")
if obj_type == "functiongroup":
print(f"\n → 正在创建函数组...")
try:
created_uri = client.create_function_group(name, description, corr_nr)
except Exception as e:
print(f" ✗ 创建失败: {e}")
raise CreateError(str(e)) from e
print(f" ✓ 函数组创建成功")
print(f" ✓ URI: {created_uri}")
print(f"\n{name} 创建完成!")
print(f" 类型: 函数组(FUNCTION GROUP)")
print(f" 描述: {description}")
return
if obj_type in DDIC_TYPES:
_create_ddic(args, client, obj_type, name, description, corr_nr, definition_file)
return
if source_file:
with open(source_file, "r", encoding="utf-8") as f:
source = f.read()
else:
template_name = name.split("/", 1)[-1].upper() if obj_type == "function" else name.upper()
source = DEFAULT_TEMPLATES[obj_type].format(name=template_name)
print(f"\n → 正在创建对象...")
try:
created_uri, created_src_uri = client.create_object(
obj_type, name, description, corr_nr, source
)
except Exception as e:
print(f" ✗ 创建失败: {e}")
raise CreateError(str(e)) from e
print(f" ✓ 对象创建成功!")
print(f" ✓ URI: {created_uri}")
if source:
print(f" ✓ 源代码已写入并激活 ({len(source)} 字符)")
print(f"\n{display_name} 创建完成!")
print(f" 类型: {type_label}")
print(f" 描述: {description}")
# 更新清单(如果适用)
_update_manifest_after_create(args, name, obj_type, corr_nr)
def _update_manifest_after_create(
args: argparse.Namespace,
name: str,
obj_type: str,
corr_nr: str | None,
) -> None:
"""create 成功后尝试写入清单(如果适用)。"""
from datetime import datetime, timezone
project_path = getattr(args, "project_path", None)
if not project_path:
return
manifest_path = os.path.join(project_path, "manifest.json")
if not os.path.isfile(manifest_path):
return
try:
manifest = Manifest.load(project_path)
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
# 根据类型推断文件路径
from sapcli.scanner import DIRECTORY_TYPE_MAP
type_to_dir = {v: k for k, v in DIRECTORY_TYPE_MAP.items()}
dir_name = type_to_dir.get(obj_type, "")
if obj_type == "function" and "/" in name:
group, func = name.split("/", 1)
rel_file = f"functions/{group.lower()}/{func.lower()}.abap"
elif dir_name:
file_base = name.split("/", 1)[-1].lower()
rel_file = f"{dir_name}/{file_base}.abap"
else:
rel_file = ""
manifest.upsert(ManifestEntry(
name=name,
type=obj_type,
file=rel_file,
system_status="active",
corr_nr=corr_nr,
depends_on=[],
last_sync=now,
last_sync_result="success",
))
manifest.save()
logger.info("清单已更新: 新增 %s", name)
except Exception as e:
logger.warning("更新清单失败: %s", e)
def _create_ddic(
args: argparse.Namespace,
client: ADTClient,
obj_type: str,
name: str,
description: str,
corr_nr: str | None,
definition_file: str | None,
) -> None:
"""创建 DDIC 对象(domain/dataelement/table/structure/tabletype)。"""
from sapcli.ddic import (
DomainDefinition,
DataElementDefinition,
TableDefinition,
StructureDefinition,
TableTypeDefinition,
TableField,
)
type_label = get_type_config(obj_type).label
definition_body: str | None = None
if definition_file:
import json
with open(definition_file, "r", encoding="utf-8") as f:
data = json.load(f)
if obj_type == "domain":
defn = DomainDefinition(
datatype=data.get("datatype", "CHAR"),
length=data.get("length", 10),
decimals=data.get("decimals", 0),
lowercase=data.get("lowercase", False),
fix_values=data.get("fix_values", []),
)
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "dataelement":
defn = DataElementDefinition(
datatype=data.get("datatype", "CHAR"),
length=data.get("length", 10),
decimals=data.get("decimals", 0),
domain_name=data.get("domain_name", ""),
)
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "table":
fields = [
TableField(
name=f.get("name", ""),
type_name=f.get("type", ""),
is_key=f.get("key", False),
not_null=f.get("not_null", False),
)
for f in data.get("fields", [])
]
defn = TableDefinition(
fields=fields,
enhancement_category=data.get("enhancement_category", "#NOT_CLASSIFIED"),
delivery_class=data.get("delivery_class", "#A"),
data_maintenance=data.get("data_maintenance", "#LIMITED"),
table_category=data.get("table_category", "#TRANSPARENT"),
)
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "structure":
fields = [
TableField(
name=f.get("name", ""),
type_name=f.get("type", ""),
is_key=f.get("key", False),
not_null=f.get("not_null", False),
)
for f in data.get("fields", [])
]
defn = StructureDefinition(
fields=fields,
enhancement_category=data.get("enhancement_category", "#NOT_CLASSIFIED"),
)
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "tabletype":
defn = TableTypeDefinition(
line_type=data.get("line_type", ""),
key_type=data.get("key_type", "#USER_DEFINED"),
access_mode=data.get("access_mode", "#STANDARD"),
)
definition_body = defn.to_xml(name.upper(), description)
if definition_body is None:
if obj_type == "domain":
defn = DomainDefinition()
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "dataelement":
defn = DataElementDefinition()
definition_body = defn.to_xml(name.upper(), description)
elif obj_type == "table":
defn = TableDefinition(fields=[
TableField(name="key_field", type_name="char10", not_null=True),
])
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "structure":
defn = StructureDefinition(fields=[
TableField(name="field1", type_name="char10"),
])
definition_body = defn.to_ddl(name.lower(), description)
elif obj_type == "tabletype":
print(f" ✗ tabletype 需要通过 --definition 指定 line_type")
raise CreateError("tabletype 需要通过 --definition 指定 line_type")
print(f"\n → 正在创建 DDIC 对象...")
try:
created_uri, created_src_uri = client.create_ddic_object(
obj_type, name, definition_body, corr_nr
)
except Exception as e:
print(f" ✗ 创建失败: {e}")
raise CreateError(str(e)) from e
print(f" ✓ DDIC 对象创建并激活成功!")
print(f" ✓ URI: {created_uri}")
print(f"\n{name} 创建完成!")
print(f" 类型: {type_label}")
print(f" 描述: {description}")