Files
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

204 lines
7.7 KiB
Python

"""TransportMixin — transport request management."""
from __future__ import annotations
import logging
import re
import xml.etree.ElementTree as ET
from sapcli.exceptions import CreateError, SapCliError
logger = logging.getLogger("sapcli.client")
class TransportMixin:
"""Transport-request CRUD and release."""
def get_transport_request(self) -> str | None:
url = f"{self.host}/sap/bc/adt/cts/transportrequests"
hdrs: dict[str, str] = {
**self._headers(),
"Accept": "application/vnd.sap.adt.transportorganizer.v1+xml",
}
logger.info("GET TRANSPORT: GET %s", url)
resp = self.session.get(url, headers=hdrs)
logger.info("GET TRANSPORT RESPONSE: HTTP %s", resp.status_code)
resp.raise_for_status()
root = ET.fromstring(resp.content)
tm_ns = "http://www.sap.com/cts/adt/tm"
for req in root.findall(f".//{{{tm_ns}}}request"):
num = req.attrib.get(f"{{{tm_ns}}}number", "")
status = req.attrib.get(f"{{{tm_ns}}}status", "")
if status == "D" and num:
logger.info("Transport request: %s", num)
return num
return None
def list_transport_requests(self) -> list[dict[str, str]]:
"""列出所有可修改的传输请求。
Returns:
列表,每项包含 number, description, owner, status 字段。
"""
url = f"{self.host}/sap/bc/adt/cts/transportrequests"
hdrs: dict[str, str] = {
**self._headers(),
"Accept": "application/vnd.sap.adt.transportorganizer.v1+xml",
}
logger.info("LIST TRANSPORT: GET %s", url)
resp = self.session.get(url, headers=hdrs)
logger.info("LIST TRANSPORT RESPONSE: HTTP %s", resp.status_code)
resp.raise_for_status()
root = ET.fromstring(resp.content)
tm_ns = "http://www.sap.com/cts/adt/tm"
results: list[dict[str, str]] = []
for req in root.findall(f".//{{{tm_ns}}}request"):
num = req.attrib.get(f"{{{tm_ns}}}number", "")
status = req.attrib.get(f"{{{tm_ns}}}status", "")
desc = req.attrib.get(f"{{{tm_ns}}}description", "")
owner = req.attrib.get(f"{{{tm_ns}}}owner", "")
if status == "D" and num:
results.append(
{
"number": num,
"description": desc,
"owner": owner,
"status": status,
}
)
logger.info(
"Transport request: %s%s (owner: %s)", num, desc, owner
)
return results
def create_transport_request(self, description: str) -> str:
"""创建新的传输请求。
Args:
description: 传输请求描述。
Returns:
新创建的传输请求编号。
"""
url = f"{self.host}/sap/bc/adt/cts/transportrequests"
body = (
'<?xml version="1.0" encoding="utf-8"?>'
'<cts:request xmlns:cts="http://www.sap.com/adt/cts"'
f' cts:type="K"'
f' cts:description="{description}"/>'
)
hdrs = self._headers()
logger.info("CREATE TRANSPORT: POST %s desc=%s", url, description)
resp = self.session.post(url, headers=hdrs, data=body.encode("utf-8"))
logger.info("CREATE TRANSPORT RESPONSE: HTTP %s", resp.status_code)
if resp.status_code >= 400:
logger.error("CREATE TRANSPORT ERROR: %s", resp.text[:2000])
raise CreateError(
f"创建传输请求失败: HTTP {resp.status_code}{resp.text[:500]}"
)
# 从响应中解析新请求编号
root = ET.fromstring(resp.content)
tm_ns = "http://www.sap.com/cts/adt/tm"
for req in root.findall(f".//{{{tm_ns}}}request"):
num = req.attrib.get(f"{{{tm_ns}}}number", "")
if num:
logger.info("New transport request created: %s", num)
return num
# 尝试从 Location header 或响应体文本中提取编号
location = resp.headers.get("Location", "")
m = re.search(r"transportrequests/(\w+)", location)
if m:
logger.info("New transport request from Location: %s", m.group(1))
return m.group(1)
logger.warning("无法从响应中解析新传输请求编号")
return ""
def transport_info(self, corr_nr: str) -> dict[str, str]:
"""获取传输请求详情。
Args:
corr_nr: 传输请求编号。
Returns:
字典含 ``number``, ``description``, ``status``, ``owner`` 等。
"""
url = f"{self.host}/sap/bc/adt/cts/transportrequests/{corr_nr}"
hdrs: dict[str, str] = {
**self._headers(),
"Accept": "application/vnd.sap.adt.transportorganizer.v1+xml",
}
logger.info("TRANSPORT INFO: GET %s", url)
resp = self.session.get(url, headers=hdrs)
logger.info("TRANSPORT INFO RESPONSE: HTTP %s", resp.status_code)
resp.raise_for_status()
root = ET.fromstring(resp.content)
tm_ns = "http://www.sap.com/cts/adt/tm"
info: dict[str, str] = {
"number": corr_nr,
"description": "",
"status": "",
"owner": "",
}
for req in root.findall(f".//{{{tm_ns}}}request"):
info["number"] = req.attrib.get(f"{{{tm_ns}}}number", corr_nr)
info["description"] = req.attrib.get(f"{{{tm_ns}}}description", "")
info["status"] = req.attrib.get(f"{{{tm_ns}}}status", "")
info["owner"] = req.attrib.get(f"{{{tm_ns}}}owner", "")
break
return info
def transport_release(self, corr_nr: str) -> bool:
"""释放传输请求。
Args:
corr_nr: 传输请求编号。
Returns:
是否成功。
"""
url = f"{self.host}/sap/bc/adt/cts/transportrequests/{corr_nr}/%20"
hdrs = self._headers()
params: dict[str, str] = {"_action": "RELEASE"}
logger.info("TRANSPORT RELEASE: POST %s corrNr=%s", url, corr_nr)
resp = self.session.post(url, headers=hdrs, params=params, data="")
logger.info("TRANSPORT RELEASE RESPONSE: HTTP %s", resp.status_code)
if resp.status_code >= 400:
logger.error("TRANSPORT RELEASE ERROR: %s", resp.text[:2000])
raise SapCliError(
f"释放传输请求失败: HTTP {resp.status_code}{resp.text[:500]}"
)
return True
def transport_objects(self, corr_nr: str) -> list[dict[str, str]]:
"""列出传输请求中的对象。
Args:
corr_nr: 传输请求编号。
Returns:
列表,每项含 ``name``, ``type``。
"""
url = f"{self.host}/sap/bc/adt/cts/transportrequests/{corr_nr}"
hdrs: dict[str, str] = {
**self._headers(),
"Accept": "application/vnd.sap.adt.transportorganizer.v1+xml",
}
params: dict[str, str] = {"withObjects": "true"}
logger.info("TRANSPORT OBJECTS: GET %s", url)
resp = self.session.get(url, headers=hdrs, params=params)
logger.info("TRANSPORT OBJECTS RESPONSE: HTTP %s", resp.status_code)
resp.raise_for_status()
root = ET.fromstring(resp.content)
results: list[dict[str, str]] = []
core_ns = "http://www.sap.com/adt/core"
for ref in root.findall(f".//{{{core_ns}}}objectReference"):
obj_name = ref.attrib.get(f"{{{core_ns}}}name", "")
obj_type = ref.attrib.get(f"{{{core_ns}}}type", "")
results.append({"name": obj_name, "type": obj_type})
return results