"""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}")