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

278 lines
10 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass, field
from sapcli.utils.xml_utils import ddl_escape as _ddl_escape
from sapcli.utils.xml_utils import xml_escape as _xml_escape
@dataclass
class DomainDefinition:
datatype: str = "CHAR"
length: int = 10
decimals: int = 0
lowercase: bool = False
fix_values: list[dict[str, str]] = field(default_factory=list)
def to_xml(self, name: str, description: str, package: str = "$TMP") -> str:
ns = (
'xmlns:doma="http://www.sap.com/dictionary/domain"'
' xmlns:adtcore="http://www.sap.com/adt/core"'
)
header = (
f'<doma:domain {ns}'
f' adtcore:name="{_xml_escape(name)}"'
f' adtcore:type="DOMA/DD"'
f' adtcore:description="{_xml_escape(description)}"'
f' adtcore:language="EN">'
f'<adtcore:packageRef adtcore:name="{_xml_escape(package)}"/>'
)
type_info = (
"<doma:content>"
"<doma:typeInformation>"
f"<doma:datatype>{self.datatype}</doma:datatype>"
f"<doma:length>{self.length:06d}</doma:length>"
f"<doma:decimals>{self.decimals:06d}</doma:decimals>"
"</doma:typeInformation>"
"<doma:outputInformation>"
f"<doma:length>{self.length:06d}</doma:length>"
"<doma:style>00</doma:style>"
"<doma:conversionExit/>"
f"<doma:signExists>false</doma:signExists>"
f"<doma:lowercase>{'true' if self.lowercase else 'false'}</doma:lowercase>"
"<doma:ampmFormat>false</doma:ampmFormat>"
"</doma:outputInformation>"
)
fix_xml = ""
if self.fix_values:
fix_xml = "<doma:valueInformation><doma:valueTableRef/><doma:appendExists>false</doma:appendExists><doma:fixValues>"
for i, fv in enumerate(self.fix_values, 1):
low = fv.get("low", "")
high = fv.get("high", "")
text = fv.get("text", "")
fix_xml += (
f"<doma:fixValue>"
f"<doma:position>{i:04d}</doma:position>"
f"<doma:low>{low}</doma:low>"
f"<doma:high>{high}</doma:high>"
f"<doma:text>{text}</doma:text>"
f"</doma:fixValue>"
)
fix_xml += "</doma:fixValues></doma:valueInformation>"
return f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{header}{type_info}{fix_xml}</doma:content></doma:domain>"
@dataclass
class DataElementDefinition:
datatype: str = "CHAR"
length: int = 10
decimals: int = 0
domain_name: str = ""
def to_xml(self, name: str, description: str, package: str = "$TMP") -> str:
ns = (
'xmlns:blue="http://www.sap.com/wbobj/dictionary/dtel"'
' xmlns:adtcore="http://www.sap.com/adt/core"'
)
header = (
f'<blue:wbobj {ns}'
f' adtcore:name="{_xml_escape(name)}"'
f' adtcore:type="DTEL/DE"'
f' adtcore:description="{_xml_escape(description)}"'
f' adtcore:language="EN">'
f'<adtcore:packageRef adtcore:name="{_xml_escape(package)}"/>'
)
type_block = (
'<dtel:dataElement xmlns:dtel="http://www.sap.com/adt/dictionary/dataelements">'
)
if self.domain_name:
type_block += (
f"<dtel:typeKind>domain</dtel:typeKind>"
f"<dtel:typeName>{self.domain_name}</dtel:typeName>"
"<dtel:dataType/>"
"<dtel:dataTypeLength/>"
"<dtel:dataTypeDecimals/>"
)
else:
type_block += (
"<dtel:typeKind>predefinedAbapType</dtel:typeKind>"
"<dtel:typeName/>"
f"<dtel:dataType>{self.datatype}</dtel:dataType>"
f"<dtel:dataTypeLength>{self.length:06d}</dtel:dataTypeLength>"
f"<dtel:dataTypeDecimals>{self.decimals:06d}</dtel:dataTypeDecimals>"
)
type_block += (
"<dtel:shortFieldLabel/>"
"<dtel:shortFieldLength>00</dtel:shortFieldLength>"
"<dtel:shortFieldMaxLength>10</dtel:shortFieldMaxLength>"
"<dtel:mediumFieldLabel/>"
"<dtel:mediumFieldLength>00</dtel:mediumFieldLength>"
"<dtel:mediumFieldMaxLength>20</dtel:mediumFieldMaxLength>"
"<dtel:longFieldLabel/>"
"<dtel:longFieldLength>00</dtel:longFieldLength>"
"<dtel:longFieldMaxLength>40</dtel:longFieldMaxLength>"
"<dtel:headingFieldLabel/>"
"<dtel:headingFieldLength>00</dtel:headingFieldLength>"
"<dtel:headingFieldMaxLength>55</dtel:headingFieldMaxLength>"
"<dtel:searchHelp/>"
"<dtel:searchHelpParameter/>"
"<dtel:setGetParameter/>"
"<dtel:defaultComponentName/>"
"<dtel:deactivateInputHistory>false</dtel:deactivateInputHistory>"
"<dtel:changeDocument>false</dtel:changeDocument>"
"<dtel:leftToRightDirection>false</dtel:leftToRightDirection>"
"<dtel:deactivateBIDIFiltering>false</dtel:deactivateBIDIFiltering>"
"</dtel:dataElement>"
)
return f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{header}{type_block}</blue:wbobj>"
@dataclass
class TableField:
name: str
type_name: str
is_key: bool = False
not_null: bool = False
def to_ddl(self) -> str:
parts = [f" {self.name:40s}: {self.type_name}"]
if self.not_null:
parts.append("not null")
return " ".join(parts) + ";"
@dataclass
class TableDefinition:
fields: list[TableField] = field(default_factory=list)
label: str = ""
enhancement_category: str = "#NOT_CLASSIFIED"
delivery_class: str = "#A"
data_maintenance: str = "#LIMITED"
table_category: str = "#TRANSPARENT"
def to_ddl(self, name: str, description: str) -> str:
lines = []
lines.append(f"@EndUserText.label : '{_ddl_escape(description)}'")
lines.append(f"@AbapCatalog.enhancementCategory : {self.enhancement_category}")
lines.append(f"@AbapCatalog.tableCategory : {self.table_category}")
lines.append(f"@AbapCatalog.deliveryClass : {self.delivery_class}")
lines.append(f"@AbapCatalog.dataMaintenance : {self.data_maintenance}")
lines.append(f"define table {name.lower()} {{")
for f in self.fields:
lines.append(f.to_ddl())
lines.append("}")
return "\n".join(lines)
@dataclass
class StructureDefinition:
fields: list[TableField] = field(default_factory=list)
enhancement_category: str = "#NOT_CLASSIFIED"
def to_ddl(self, name: str, description: str) -> str:
lines = []
lines.append(f"@EndUserText.label : '{_ddl_escape(description)}'")
lines.append(f"@AbapCatalog.enhancementCategory : {self.enhancement_category}")
lines.append(f"define structure {name.lower()} {{")
for f in self.fields:
lines.append(f.to_ddl())
lines.append("}")
return "\n".join(lines)
@dataclass
class TableTypeDefinition:
line_type: str = ""
key_type: str = "#USER_DEFINED"
access_mode: str = "#STANDARD"
key_fields: list[str] = field(default_factory=list)
def to_xml(self, name: str, description: str, package: str = "$TMP") -> str:
ns = (
'xmlns:ttyp="http://www.sap.com/adt/dictionary/tabletypes"'
' xmlns:adtcore="http://www.sap.com/adt/core"'
)
header = (
f'<ttyp:tableType {ns}'
f' adtcore:name="{_xml_escape(name)}"'
f' adtcore:type="TTYP"'
f' adtcore:description="{_xml_escape(description)}"'
f' adtcore:language="EN">'
f'<adtcore:packageRef adtcore:name="{_xml_escape(package)}"/>'
)
body = (
"<ttyp:content>"
"<ttyp:accessMode>"
f"<ttyp:mode>{self.access_mode}</ttyp:mode>"
"</ttyp:accessMode>"
"<ttyp:keyDefinition>"
f"<ttyp:definition>{self.key_type}</ttyp:definition>"
"</ttyp:keyDefinition>"
"<ttyp:rowType>"
f"<ttyp:typeKind>structured</ttyp:typeKind>"
f"<ttyp:typeName>{self.line_type}</ttyp:typeName>"
"</ttyp:rowType>"
"</ttyp:content>"
)
return f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{header}{body}</ttyp:tableType>"
def load_definition_from_json(path: str, obj_type: str) -> str | None:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return build_definition(data, obj_type)
def build_definition(data: dict, obj_type: str) -> str | None:
if obj_type == "domain":
return _build_domain(data)
elif obj_type == "dataelement":
return _build_dataelement(data)
elif obj_type in ("table", "structure"):
return None
elif obj_type == "tabletype":
return _build_tabletype(data)
return None
def _build_domain(data: dict) -> str:
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", []),
)
name = data.get("name", "ZDOMAIN")
description = data.get("description", name)
package = data.get("package", "$TMP")
return defn.to_xml(name, description, package)
def _build_dataelement(data: dict) -> str:
defn = DataElementDefinition(
datatype=data.get("datatype", "CHAR"),
length=data.get("length", 10),
decimals=data.get("decimals", 0),
domain_name=data.get("domain_name", ""),
)
name = data.get("name", "ZDATAELEMENT")
description = data.get("description", name)
package = data.get("package", "$TMP")
return defn.to_xml(name, description, package)
def _build_tabletype(data: dict) -> str:
defn = TableTypeDefinition(
line_type=data.get("line_type", ""),
key_type=data.get("key_type", "#USER_DEFINED"),
access_mode=data.get("access_mode", "#STANDARD"),
key_fields=data.get("key_fields", []),
)
name = data.get("name", "ZTABLETYPE")
description = data.get("description", name)
package = data.get("package", "$TMP")
return defn.to_xml(name, description, package)