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
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user