cmd_create 在未指定 --corr_nr 时无条件调 _select_transport_request(), 未判目标包 —— $TMP 是本地包(对象不进传输),弹交互是错的,且管道/脚本 调用时 input() 直接 EOFError,报错栈指向传输选择,误导排查方向。 修法:包名规范化后等于 $TMP/TMP 则跳过选择(corr_nr=None)。 实测:create --package '$TMP' 直接成功,TADIR 落 DEVCLASS=$TMP。
1161 lines
44 KiB
Python
1161 lines
44 KiB
Python
"""CRUD 命令:download / sync / info / delete / create。"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import logging
|
||
import os
|
||
import re
|
||
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_r3tr_object_code, get_type_config, parse_object_name
|
||
|
||
DDIC_TYPES = {"domain", "dataelement", "table", "structure", "tabletype"}
|
||
|
||
# NW 7.40 ADT 对 DDIC 对象(structure/table/tabletype/dataelement/domain)的 */* 响应
|
||
# 不含 description / packageRef,需回退查文本表补描述、查 TADIR 补开发包。
|
||
# 描述表的语言列固定为 DDLANGUAGE、文本列固定为 DDTEXT。
|
||
_DDIC_DESC_TABLE: dict[str, tuple[str, str]] = {
|
||
"structure": ("DD02T", "TABNAME"),
|
||
"table": ("DD02T", "TABNAME"),
|
||
"tabletype": ("DD40T", "TYPENAME"),
|
||
"dataelement": ("DD04T", "ROLLNAME"),
|
||
"domain": ("DD01T", "DOMNAME"),
|
||
}
|
||
|
||
# DDIC 对象 → TADIR.OBJECT 代码(用于补开发包 DEVCLASS)。
|
||
_DDIC_PACKAGE_OBJECT: dict[str, str] = {
|
||
"structure": "TABL",
|
||
"table": "TABL",
|
||
"tabletype": "TTYP",
|
||
"dataelement": "DTEL",
|
||
"domain": "DOMA",
|
||
}
|
||
|
||
# ADT masterLanguage 返回 ISO 码(如 'ZH'/'EN'),文本表 DDLANGUAGE 用 SAP 单字符键。
|
||
_ISO_TO_SAP_LANGUAGE: dict[str, str] = {
|
||
"ZH": "1",
|
||
"EN": "E",
|
||
"DE": "D",
|
||
"JA": "J",
|
||
"KO": "3",
|
||
}
|
||
|
||
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'
|
||
),
|
||
# 以下类型不提供完整业务骨架,仅给出占位注释。
|
||
# 建议通过 --source 指定本地源码文件,或用 --definition 创建 DDIC 定义。
|
||
"include": (
|
||
'*&---------------------------------------------------------------------*\n'
|
||
'*& Include {name}\n'
|
||
'*&---------------------------------------------------------------------*\n'
|
||
'* Include 程序骨架:在此编写可被其它程序 INCLUDE 的 ABAP 片段。\n'
|
||
'* 提示:用 --source 指定本地源码文件可覆盖本模板。\n'
|
||
),
|
||
"messageclass": (
|
||
'* Message Class {name}\n'
|
||
'* 消息类(T100)不包含 ABAP 源码,消息条目通过 SE91 维护。\n'
|
||
'* 提示:建议用 --definition 创建,或通过 SAP GUI (SE91) 维护消息。\n'
|
||
),
|
||
"view": (
|
||
'* Database View {name}\n'
|
||
'* 数据库视图 DDL 骨架(基础 SELECT 形式,按需替换):\n'
|
||
'* SELECT <字段列表>\n'
|
||
'* FROM <主表>\n'
|
||
'* [JOIN <关联表> ON <条件>]\n'
|
||
'* [WHERE <过滤条件>]\n'
|
||
'* 提示:用 --source 指定完整视图定义可覆盖本模板。\n'
|
||
),
|
||
"searchhelp": (
|
||
'* Search Help {name}\n'
|
||
'* 搜索帮助不包含 ABAP 源码,定义含数据源与显示字段。\n'
|
||
'* 提示:建议用 --definition 创建,或通过 SAP GUI (SE11) 维护。\n'
|
||
),
|
||
"lockobject": (
|
||
'* Lock Object {name}\n'
|
||
'* 锁对象不包含 ABAP 源码,定义含锁定模式与主表。\n'
|
||
'* 提示:建议用 --definition 创建,或通过 SAP GUI (SE11) 维护。\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.exists_uri or 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.exists_uri or 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 _query_transport_request(
|
||
client: ADTClient, obj_type: str, display_name: str
|
||
) -> str:
|
||
"""查对象当前所在(未释放)的传输请求号。纯只读查 E071;失败优雅降级。"""
|
||
r3tr_code = get_r3tr_object_code(obj_type)
|
||
if not r3tr_code:
|
||
return "—(非独立传输对象)"
|
||
obj_name_upper = display_name.upper().split("/")[-1]
|
||
if not re.fullmatch(r"[A-Za-z0-9_/$]+", obj_name_upper):
|
||
return "(对象名非法,跳过)"
|
||
try:
|
||
sql = (
|
||
f"SELECT trkorr FROM e071 WHERE pgmid IN ('R3TR') "
|
||
f"AND object IN ('{r3tr_code}') AND obj_name IN ('{obj_name_upper}') "
|
||
f"UP TO 10 ROWS"
|
||
)
|
||
result = client.query_table_data(sql, max_rows=10)
|
||
trkorrs = sorted({row[0] for row in result.get("rows", []) if row and row[0]})
|
||
return ", ".join(trkorrs) if trkorrs else "无"
|
||
except Exception:
|
||
logger.debug("查询 E071 传输请求失败", exc_info=True)
|
||
return "(无法获取)"
|
||
|
||
|
||
def _sap_language_key(master_language: str) -> str:
|
||
"""把 ADT masterLanguage(ISO 码,如 'ZH'/'EN')转成 SAP 单字符语言键。
|
||
|
||
已是单字符时原样返回;无法映射时原样返回(回退顺序会自然跳过不匹配项)。
|
||
"""
|
||
lang = (master_language or "").strip().upper()
|
||
if not lang:
|
||
return ""
|
||
if len(lang) == 1:
|
||
return lang
|
||
return _ISO_TO_SAP_LANGUAGE.get(lang, lang)
|
||
|
||
|
||
def _pick_description_text(rows, preferred_lang: str = "") -> str:
|
||
"""按「登录语言 -> 'E' -> '1' -> 首行」回退,从 (语言, 文本) 行中挑描述文本。"""
|
||
if not rows:
|
||
return ""
|
||
by_lang: dict[str, str] = {}
|
||
order: list[str] = []
|
||
for lang, text in rows:
|
||
lang = (lang or "").strip().upper()
|
||
if lang not in by_lang:
|
||
by_lang[lang] = (text or "").strip()
|
||
order.append(lang)
|
||
priority = [p for p in (preferred_lang, "E", "1") if p]
|
||
for p in priority:
|
||
p = p.strip().upper()
|
||
for lang in order:
|
||
if lang == p and by_lang[lang]:
|
||
return by_lang[lang]
|
||
for lang in order:
|
||
if by_lang[lang]:
|
||
return by_lang[lang]
|
||
return ""
|
||
|
||
|
||
def _query_ddic_description(
|
||
client: ADTClient, obj_type: str, obj_name: str, preferred_lang: str
|
||
) -> str:
|
||
"""ADT 返回空描述时,查描述表补 DDIC 对象描述;失败返回 '' 不抛。"""
|
||
if obj_type not in _DDIC_DESC_TABLE:
|
||
return ""
|
||
table, key_col = _DDIC_DESC_TABLE[obj_type]
|
||
name_upper = obj_name.upper().split("/")[-1]
|
||
if not re.fullmatch(r"[A-Za-z0-9_/$]+", name_upper):
|
||
return ""
|
||
try:
|
||
sql = (
|
||
f"SELECT DDLANGUAGE, DDTEXT FROM {table} "
|
||
f"WHERE {key_col} IN ('{name_upper}')"
|
||
)
|
||
result = client.query_table_data(sql, max_rows=20)
|
||
columns = result.get("columns", [])
|
||
rows = result.get("rows", [])
|
||
lang_idx = columns.index("DDLANGUAGE") if "DDLANGUAGE" in columns else 0
|
||
text_idx = columns.index("DDTEXT") if "DDTEXT" in columns else 1
|
||
pairs = []
|
||
for row in rows:
|
||
if row is None:
|
||
continue
|
||
lang = row[lang_idx] if lang_idx < len(row) else ""
|
||
text = row[text_idx] if text_idx < len(row) else ""
|
||
pairs.append((lang, text))
|
||
return _pick_description_text(pairs, preferred_lang)
|
||
except Exception:
|
||
logger.debug("查询 %s 描述失败", table, exc_info=True)
|
||
return ""
|
||
|
||
|
||
def _query_ddic_package(client: ADTClient, obj_type: str, obj_name: str) -> str:
|
||
"""ADT 返回空开发包时,查 TADIR 补 DEVCLASS;失败返回 '' 不抛。"""
|
||
if obj_type not in _DDIC_PACKAGE_OBJECT:
|
||
return ""
|
||
obj_code = _DDIC_PACKAGE_OBJECT[obj_type]
|
||
name_upper = obj_name.upper().split("/")[-1]
|
||
if not re.fullmatch(r"[A-Za-z0-9_/$]+", name_upper):
|
||
return ""
|
||
try:
|
||
sql = (
|
||
f"SELECT DEVCLASS FROM TADIR "
|
||
f"WHERE OBJECT IN ('{obj_code}') AND OBJ_NAME IN ('{name_upper}')"
|
||
)
|
||
result = client.query_table_data(sql, max_rows=10)
|
||
columns = result.get("columns", [])
|
||
rows = result.get("rows", [])
|
||
dev_idx = columns.index("DEVCLASS") if "DEVCLASS" in columns else 0
|
||
for row in rows:
|
||
if row and dev_idx < len(row) and row[dev_idx]:
|
||
return str(row[dev_idx]).strip()
|
||
return ""
|
||
except Exception:
|
||
logger.debug("查询 TADIR 开发包失败", exc_info=True)
|
||
return ""
|
||
|
||
|
||
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": "*/*",
|
||
"structure": "application/vnd.sap.adt.ddic.structures.v1+xml",
|
||
"include": "application/vnd.sap.adt.programs.includes.v2+xml",
|
||
"cdsview": "application/vnd.sap.adt.ddic.ddls.v3+xml",
|
||
"messageclass": "application/vnd.sap.adt.t100.message.classes.v1+xml",
|
||
"view": "application/vnd.sap.adt.ddic.views.v1+xml",
|
||
"searchhelp": "application/vnd.sap.adt.ddic.searchhelps.v1+xml",
|
||
"lockobject": "application/vnd.sap.adt.ddic.lockobjects.v1+xml",
|
||
"dcl": "text/plain",
|
||
"ddlX": "text/plain",
|
||
}
|
||
|
||
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:
|
||
# NW 7.40 实测:DDIC 专属媒体类型返回 406,且 application/xml 本身也 406
|
||
# (structure 专属头 → 406、application/xml → 406、*/* → 200)。
|
||
# 回退必须是 */*,否则回退恒败、命令报「查询失败: HTTP 406」。
|
||
hdrs["Accept"] = "*/*"
|
||
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", "")
|
||
|
||
# 开发包:解析对象 XML 自带的 packageRef 子元素
|
||
pkg_ref = root.find(f".//{{{ns['adtcore']}}}packageRef")
|
||
info_package = (
|
||
pkg_ref.attrib.get(f"{{{ns['adtcore']}}}name", "")
|
||
if pkg_ref is not None
|
||
else ""
|
||
)
|
||
|
||
# NW 7.40 DDIC 的 */* 响应不含 description/packageRef:仅在 ADT 返回值为空时补查,
|
||
# 补查失败静默降级(保持原样),不打断 info 输出。
|
||
if not info_desc:
|
||
info_desc = _query_ddic_description(
|
||
client, obj_type, parsed.display_name, _sap_language_key(info_language)
|
||
)
|
||
if not info_package:
|
||
info_package = _query_ddic_package(client, obj_type, parsed.display_name)
|
||
|
||
if info_version == "active":
|
||
status_icon = "✓ 已激活"
|
||
elif info_version == "inactive":
|
||
status_icon = "⚠️ 未激活"
|
||
else:
|
||
status_icon = f"❓ {info_version}"
|
||
|
||
# 传输请求号:纯只读查 E071(不在对象元数据 XML 中)
|
||
corr_display = _query_transport_request(client, obj_type, parsed.display_name)
|
||
|
||
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_package or '(未指定)'}")
|
||
print(f" 传输请求: {corr_display}")
|
||
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.exists_uri or 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 cmd_unlock(args: argparse.Namespace, client: ADTClient) -> None:
|
||
"""释放指定对象的 enqueue 锁(清理 delete 后残留锁 / SM12 场景)。"""
|
||
obj_type: str = args.type
|
||
name: str = args.name
|
||
lock_handle: str = args.lock_handle
|
||
|
||
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 → 正在释放锁 (lockHandle={lock_handle[:12]}...)...")
|
||
ok = client.unlock(parsed.obj_uri, lock_handle)
|
||
if ok:
|
||
print(" ✓ 锁释放成功")
|
||
else:
|
||
print(" ✗ 锁释放失败(HTTP 非 200,可能锁已不存在或 handle 无效)")
|
||
raise SapCliError(f"解锁失败: {parsed.display_name}")
|
||
|
||
|
||
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)
|
||
|
||
# JSON definition description: --description wins, then JSON, then name.
|
||
# (NW740 ADT create does not persist texts anyway — see SKILL.md known
|
||
# limitation; this only affects the XML body sent at creation time.)
|
||
if (
|
||
definition_file
|
||
and not getattr(args, "description", None)
|
||
and obj_type in DDIC_TYPES
|
||
):
|
||
import json as _json
|
||
import os as _os
|
||
|
||
if _os.path.isfile(definition_file):
|
||
try:
|
||
with open(definition_file, "r", encoding="utf-8") as f:
|
||
_data = _json.load(f)
|
||
_json_desc = _data.get("description")
|
||
if _json_desc:
|
||
description = _json_desc
|
||
except Exception as _e:
|
||
logger.warning("读取定义文件 description 失败: %s", _e)
|
||
|
||
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.exists_uri or 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:
|
||
package_for_transport = getattr(args, "package", None) or "$TMP"
|
||
if package_for_transport.strip().upper() in ("$TMP", "TMP"):
|
||
# $TMP = 本地包,对象不进传输;弹交互选择是错的(管道调用会 EOFError)
|
||
print(" ℹ 目标包为 $TMP(本地对象),跳过传输请求选择")
|
||
corr_nr = None
|
||
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:
|
||
package = getattr(args, "package", None) or "$TMP"
|
||
_create_ddic(args, client, obj_type, name, description, corr_nr, definition_file, package)
|
||
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:
|
||
package = getattr(args, "package", None) or "$TMP"
|
||
created_uri, created_src_uri = client.create_object(
|
||
obj_type, name, description, corr_nr, source, package
|
||
)
|
||
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,
|
||
package: str = "$TMP",
|
||
) -> 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, package)
|
||
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, package)
|
||
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, package)
|
||
|
||
if definition_body is None:
|
||
if obj_type == "domain":
|
||
defn = DomainDefinition()
|
||
definition_body = defn.to_xml(name.upper(), description, package)
|
||
elif obj_type == "dataelement":
|
||
defn = DataElementDefinition()
|
||
definition_body = defn.to_xml(name.upper(), description, package)
|
||
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, package
|
||
)
|
||
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}")
|