Files
sap-cli-skill/assets/sapcli/commands/crud.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

891 lines
32 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.
"""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}")