# sap-cli — 技术说明 ## 目录 - [项目定位](#项目定位) - [架构概览](#架构概览) - [包结构](#包结构) - [模块职责](#模块职责) - [通信流程](#通信流程) - [对象类型映射](#对象类型映射) - [异常体系](#异常体系) - [配置加载策略](#配置加载策略) - [日志机制](#日志机制) - [错误处理策略](#错误处理策略) - [测试架构](#测试架构) - [依赖关系](#依赖关系) --- ## 项目定位 sap-cli 是一个轻量级命令行工具,通过 SAP ADT (ABAP Development Tools) REST API 与 SAP NetWeaver 系统交互,实现 ABAP 开发对象的远程管理(创建、同步、下载、查询、删除)。 **核心设计原则:** - 零 SAP 专有客户端依赖,仅基于标准 HTTP 协议 - 唯一第三方依赖为 `requests` - 所有通信均通过 ADT REST API 完成,数据格式为 XML / Plain Text --- ## 架构概览 ![[sap-cli/doc/assets/architecture.drawio.png]] 架构分为三层: | 层级 | 组件 | 说明 | |------|------|------| | **用户终端** | `main.py` | CLI 入口,argparse 参数解析与命令分发 | | **核心包** | `sapcli/` | 5 个模块:config / types / exceptions / client / commands | | **外部系统** | SAP NetWeaver | ADT REST API 端点(Programs / OO Objects / DDIC / CTS) | 左侧外部文件通过虚线连接至核心包: - `config.ini` — 连接配置(读取) - `.abap 文件` — 本地源代码(读写) - `log/` — 运行日志(写入) 核心通信链路:`client.py (ADTClient)` 通过 HTTP/REST(Basic Auth + CSRF Token)与 SAP 系统交互。 ## 包结构 ``` sap-cli/ ├── main.py # CLI 入口(argparse + 命令分发) ├── config.ini # 默认连接配置文件 ├── sapcli/ # 核心功能包 │ ├── __init__.py │ ├── config.py # 配置加载(INI / 环境变量) │ ├── types.py # 对象类型定义与名称解析 │ ├── client.py # ADT REST API 客户端 │ ├── commands.py # 5 个业务命令的实现 │ └── exceptions.py # 自定义异常层级 ├── test/ │ └── script/ │ └── test_main.py # 集成测试套件(子进程调用) ├── doc/ # 文档 │ ├── ADT/ # ADT 技术原理系列 │ ├── 技术说明.md # 本文档 │ ├── sap-cli使用指南.md # 用户操作指南 │ └── 测试报告.md # 测试覆盖报告 └── log/ # 运行时日志目录 ``` --- ## 模块职责 ### main.py — CLI 入口 | 组件 | 职责 | |------|------| | 参数定义 | 定义 5 个子命令(download / sync / delete / info / create)及其参数 | | 配置加载 | 调用 `sapcli.config.load_config()` 获取连接参数 | | 客户端初始化 | 创建 `ADTClient` 实例并执行登录(获取 CSRF Token) | | 命令分发 | 根据子命令名称映射到对应的 `cmd_*()` 函数 | ### sapcli/config.py — 配置管理 | 组件 | 职责 | |------|------| | `SAPConfig` | 连接参数数据类(host / client / user / password) | | `load_config()` | 按优先级链加载配置:环境变量 → 指定文件 → 工作目录 → 脚本目录 | ### sapcli/types.py — 对象类型系统 | 组件 | 职责 | |------|------| | `ObjectTypeConfig` | 单个 ABAP 对象类型的 URI 模板与元数据(数据类) | | `OBJECT_TYPE_CONFIG` | 9 种 ABAP 对象类型的注册表 | | `ParsedName` | 解析后的对象名组件(数据类) | | `parse_object_name()` | 将用户输入解析为 ADT URI 所需的路径片段 | | `get_type_config()` | 按类型键查询对象类型配置 | | `all_type_keys()` | 返回所有受支持的对象类型列表 | ### sapcli/client.py — ADT 通信层 | 方法 | 职责 | |------|------| | `login()` | Basic Auth 认证 + 获取 CSRF Token | | `object_exists()` | 检查对象是否存在于 SAP 系统 | | `get_source()` | 下载对象源代码 | | `set_source()` | 上传(写入)对象源代码 | | `lock()` / `unlock()` | 对象锁定 / 解锁(stateful session) | | `get_transport_request()` | 获取可用的传输请求号 | | `activate()` | 激活对象 | | `syntax_check()` | 语法检查 | | `create_object()` | 创建新对象(支持 9 种类型) | | `delete_object()` | 删除对象 | | `function_group_exists()` | 检查函数组是否存在 | | `create_function_group()` | 创建函数组容器 | ### sapcli/commands.py — 命令处理 | 函数 | 职责 | |------|------| | `cmd_download()` | 下载对象源码到本地 `.abap` 文件 | | `cmd_sync()` | 同步本地源码到 SAP(含自动创建、锁定、写入、检查、激活) | | `cmd_info()` | 查询并展示对象元数据 | | `cmd_delete()` | 从 SAP 系统删除对象(需用户确认) | | `cmd_create()` | 在 SAP 系统创建新对象(支持模板预填充) | ### sapcli/exceptions.py — 异常层级 | 异常类 | 触发场景 | |--------|---------| | `SapCliError` | 所有自定义异常的基类 | | `ConfigError` | 配置文件缺失或格式错误 | | `LoginError` | SAP 登录失败(认证错误或网络不通) | | `ObjectNotFoundError` | 请求的对象在 SAP 系统中不存在 | | `ObjectAlreadyExistsError` | 创建时对象已存在 | | `LockError` | 对象锁定冲突 | | `ActivationError` | 对象激活失败 | | `SyntaxCheckError` | 语法检查未通过 | | `DeleteError` | 删除操作失败 | | `CreateError` | 创建操作失败 | | `InvalidNameError` | 对象名称格式不合法 | --- ## 通信流程 ### 认证机制 采用 HTTP Basic Auth + CSRF Token 双重验证: ``` ┌────────┐ ┌────────┐ │ Client │ │ SAP │ └───┬────┘ └───┬────┘ │ GET /sap/bc/adt/compatibility/graph │ │ Header: x-csrf-token: fetch │ │ Header: Authorization: Basic *** │ │ ──────────────────────────────────────────► │ │ │ │ 200 OK │ │ Header: x-csrf-token: │ │ ◄────────────────────────────────────────── │ │ │ │ 后续所有请求携带 CSRF Token │ │ (POST / PUT / DELETE 必须携带) │ │ ──────────────────────────────────────────► │ ``` ### sync 命令 — 完整 API 调用链 `sync` 是最核心的命令,执行以下完整流程: ``` ┌─────────────────────────────────────────────────────────────┐ │ Step 1: 登录获取 CSRF Token │ │ GET /sap/bc/adt/compatibility/graph │ ├─────────────────────────────────────────────────────────────┤ │ Step 2: 检查对象存在性(不存在则自动创建) │ │ GET /sap/bc/adt/programs/programs/{name} │ ├─────────────────────────────────────────────────────────────┤ │ Step 3: 获取传输请求号 │ │ GET /sap/bc/adt/cts/transportrequests │ ├─────────────────────────────────────────────────────────────┤ │ Step 4: 锁定对象(stateful session) │ │ POST /sap/bc/adt/programs/programs/{name} │ │ ?_action=LOCK&accessMode=MODIFY │ ├─────────────────────────────────────────────────────────────┤ │ Step 5: 写入源代码 │ │ PUT /sap/bc/adt/programs/programs/{name}/source/main │ │ ?lockHandle={handle} │ ├─────────────────────────────────────────────────────────────┤ │ Step 6: 解锁对象 │ │ POST /sap/bc/adt/programs/programs/{name} │ │ ?_action=UNLOCK&lockHandle={handle} │ ├─────────────────────────────────────────────────────────────┤ │ Step 7: 语法检查(可选) │ │ POST /sap/bc/adt/activation?method=check │ ├─────────────────────────────────────────────────────────────┤ │ Step 8: 激活对象 │ │ POST /sap/bc/adt/activation?method=activate │ └─────────────────────────────────────────────────────────────┘ ``` ### 锁定与传输请求 — 容错机制 工具内置了两层自动重试策略: 1. **锁定重试** — 若对象已在其他传输请求中被锁定,从错误响应中提取已有的传输请求号,使用该传输号重新锁定 2. **写入重试** — 若写入源码时因传输号不匹配而失败,自动使用正确的传输号重试 ### Session 管理 | 操作类型 | Session 模式 | 说明 | |---------|-------------|------| | 锁定 / 写入 / 解锁 | **stateful** | 通过 `X-sap-adt-sessiontype: stateful` 头维持会话状态 | | 读取 / 激活 | **stateless** | 无状态请求,独立响应 | | 解锁后 | 自动切换回 **stateless** | 确保会话资源释放 | --- ## 对象类型映射 每种 ABAP 对象类型对应不同的 ADT URI 模板: | 类型 | 对象 URI | 源码 URI | 备注 | |------|---------|---------|------| | `report` | `/sap/bc/adt/programs/programs/{name}` | `.../source/main` | ABAP 报表程序 | | `class` | `/sap/bc/adt/oo/classes/{name}` | `.../source/main` | ABAP 类 | | `interface` | `/sap/bc/adt/oo/interfaces/{name}` | `.../source/main` | ABAP 接口 | | `function` | `/sap/bc/adt/functions/groups/{group}/fmodules/{name}` | `.../source/main` | 需 `函数组/函数名` 格式 | | `functiongroup` | `/sap/bc/adt/functions/groups/{name}` | 无源码 | 仅容器,不含源码 | | `domain` | `/sap/bc/adt/ddic/domains/{name}` | `.../source/main` | ABAP 域 | | `dataelement` | `/sap/bc/adt/ddic/dataelements/{name}` | `.../source/main` | ABAP 数据元素 | | `table` | `/sap/bc/adt/ddic/tables/{name}` | `.../source/main` | ABAP 透明表 | | `tabletype` | `/sap/bc/adt/vit/wb/object_type/ttypda/object_name/{name}` | 无源码 (VIT 端点) | ABAP 表类型,仅支持元数据查询 | > [!note] function 类型的特殊性 > `function` 类型需要 `函数组名/函数名` 格式输入(如 `ZGROUP/Z_MY_FUNC`),URI 中包含两层路径参数(`{group}` 和 `{name}`)。 --- ## 配置加载策略 配置按以下优先级链加载,高优先级源覆盖低优先级源: ``` ┌──────────────────────────────┐ │ 环境变量 (SAP_HOST 等) │ ← 最高优先级 └──────────────┬───────────────┘ │ 覆盖 ┌──────────────▼───────────────┐ │ --config 指定的 INI 文件 │ └──────────────┬───────────────┘ │ 回退 ┌──────────────▼───────────────┐ │ 当前工作目录 / config.ini │ └──────────────┬───────────────┘ │ 回退 ┌──────────────▼───────────────┐ │ 脚本同目录 / config.ini │ ← 最低优先级 └──────────────────────────────┘ ``` 支持的环境变量映射: | INI 字段 | 环境变量 | |---------|---------| | `host` | `SAP_HOST` | | `client` | `SAP_CLIENT` | | `user` | `SAP_USER` | | `password` | `SAP_PASSWORD` | --- ## 日志机制 | 属性 | 值 | |------|-----| | 日志路径 | `log/adt_tools.log` | | 日志级别 | `DEBUG`(记录所有 HTTP 请求/响应详情) | | 日志格式 | `时间 │ 级别 │ 消息` | | 写入模式 | 追加写入(append) | 日志记录每次 API 调用的完整上下文:请求 URL、请求参数、响应状态码、响应体摘要,便于生产环境问题排查。 --- ## 错误处理策略 ### 按场景分类 | 场景 | 处理方式 | 对应异常 | |------|---------|---------| | 登录失败 | 终止并报错 | `LoginError` | | 对象不存在(download / info / delete) | 提示不存在并退出 | `ObjectNotFoundError` | | 对象不存在(sync) | 自动创建空对象后继续 | — | | 对象已存在(create) | 提示已存在并退出 | `ObjectAlreadyExistsError` | | 语法检查失败 | 显示错误详情,不执行激活 | `SyntaxCheckError` | | 激活失败 | 显示错误/警告详情并退出 | `ActivationError` | | 锁定冲突 | 自动提取已有传输号重试 | `LockError` | | 写入冲突 | 自动使用正确传输号重试 | — | | 删除操作 | 需用户输入 `yes` 确认 | — | ### 错误处理设计原则 1. **快速失败** — 非预期错误立即终止,避免静默失败 2. **自动恢复** — 锁定冲突和写入冲突具备自动重试能力 3. **信息透明** — 所有错误均输出详细信息,日志记录完整上下文 --- ## 测试架构 测试套件位于 `test/script/test_main.py`,通过子进程调用 `main.py`,确保端到端验证。 ### 测试设计 - **独立性** — 每个测试场景独立运行,使用时间戳生成唯一临时对象名 - **完整性** — 每个场景覆盖完整生命周期(创建 → 验证 → 修改 → 验证 → 删除 → 验证) - **自清理** — 测试结束后自动清理临时文件(`test_output/` 目录) - **可分组** — 支持按场景组选择运行 ### 测试场景组 | 组名 | 说明 | 包含场景 | |------|------|---------| | `happy` | 正常流程 | Report / Class / Function+FunctionGroup / Interface 生命周期 | | `error` | 错误处理 | 语法错误 / 对象不存在 / 重复创建 | | `edge` | 边界条件 | sync 自动创建 / 删除确认 / DDIC 查询 | | `existing` | 已有对象操作 | 对已有对象的下载和同步 | | `config` | 配置验证 | 配置文件加载测试 | --- ## 依赖关系 ### 运行时依赖 ``` main.py ├── sapcli/ # 内部包 │ ├── config.py │ │ └── configparser # 解析 INI 配置文件 │ ├── client.py │ │ ├── requests # HTTP 通信(唯一第三方依赖) │ │ └── xml.etree # 解析 ADT XML 响应 │ ├── commands.py │ │ └── logging # 日志记录 │ ├── types.py │ │ └── re # 对象名称解析 │ └── exceptions.py │ └── (无外部依赖) ├── argparse # 命令行参数解析 └── logging # 日志配置 ``` ### 测试时依赖 ``` test/script/test_main.py ├── subprocess # 子进程调用 main.py └── 标准库(os, sys, time, shutil, re) ``` ### 第三方依赖汇总 | 依赖 | 用途 | 安装方式 | |------|------|---------| | `requests` | HTTP 通信 | `pip install requests` | > [!tip] 零额外依赖 > 除 `requests` 外,所有依赖均为 Python 标准库,无需额外安装。