- 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
89 lines
3.0 KiB
Python
89 lines
3.0 KiB
Python
"""ADTClient base — connection, authentication, and generic request helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import requests
|
|
from requests.auth import HTTPBasicAuth
|
|
|
|
from sapcli.exceptions import LoginError
|
|
|
|
logger = logging.getLogger("sapcli.client")
|
|
|
|
|
|
class ADTClientBase:
|
|
"""Connection / authentication foundation shared by all mixins."""
|
|
|
|
def __init__(
|
|
self,
|
|
host: str,
|
|
client: str,
|
|
user: str,
|
|
password: str,
|
|
verify_ssl: bool = False,
|
|
) -> None:
|
|
self.host: str = host.rstrip("/")
|
|
self.csrf_token: str = "fetch"
|
|
self.session: requests.Session = requests.Session()
|
|
self.session.auth = HTTPBasicAuth(user, password)
|
|
self.session.verify = verify_ssl
|
|
self._stateful: bool = False
|
|
self.sap_client: str = client
|
|
|
|
# ------------------------------------------------------------------
|
|
# Context-manager protocol
|
|
# ------------------------------------------------------------------
|
|
|
|
def __enter__(self): # type: ignore[override]
|
|
self.login()
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
self.session.close()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Headers / auth helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def _headers(self, content_type: str = "application/xml") -> dict[str, str]:
|
|
return {
|
|
"Accept": "*/*",
|
|
"Cache-Control": "no-cache",
|
|
"x-csrf-token": self.csrf_token,
|
|
"X-sap-adt-sessiontype": "stateful" if self._stateful else "stateless",
|
|
"content-type": content_type,
|
|
"sap-client": self.sap_client,
|
|
}
|
|
|
|
def login(self) -> bool:
|
|
url = f"{self.host}/sap/bc/adt/compatibility/graph"
|
|
hdrs = self._headers()
|
|
logger.info("LOGIN: GET %s", url)
|
|
resp = self.session.get(url, headers=hdrs)
|
|
logger.info("LOGIN RESPONSE: HTTP %s", resp.status_code)
|
|
if resp.status_code == 200:
|
|
self.csrf_token = resp.headers.get("x-csrf-token", "")
|
|
if self.csrf_token:
|
|
logger.info(
|
|
"CSRF Token: %s...(%d chars)",
|
|
self.csrf_token[:8],
|
|
len(self.csrf_token),
|
|
)
|
|
return True
|
|
raise LoginError(f"登录失败: HTTP {resp.status_code}")
|
|
|
|
def object_exists(self, obj_uri: str) -> bool:
|
|
url = f"{self.host}{obj_uri}"
|
|
hdrs = self._headers()
|
|
hdrs["Accept"] = "*/*"
|
|
logger.info("CHECK EXISTS: GET %s", url)
|
|
resp = self.session.get(url, headers=hdrs)
|
|
logger.info("CHECK EXISTS RESPONSE: HTTP %s", resp.status_code)
|
|
if resp.status_code == 200:
|
|
return True
|
|
if resp.status_code == 404:
|
|
return False
|
|
logger.warning("Unexpected status %d for existence check", resp.status_code)
|
|
return resp.status_code < 400
|