"""Direct ADT HTTP access. Only for teardown (deletion). The model never uses it.
Credentials come from the environment (or .env): A4H_URL, A4H_USER, A4H_PASSWORD, A4H_CLIENT.
"""
import base64
import http.cookiejar
import os
import re
import urllib.request
from xml.sax.saxutils import quoteattr
DEL_REQ = "application/vnd.sap.adt.deletion.request.v1+xml"
DEL_RES = "application/vnd.sap.adt.deletion.response.v1+xml"
def load_env(path=".env"):
if os.path.exists(path):
for line in open(path):
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip())
class AdtClient:
def __init__(self):
load_env()
self.base = os.environ["A4H_URL"].rstrip("/")
self.client = os.environ.get("A4H_CLIENT", "001")
auth = f'{os.environ["A4H_USER"]}:{os.environ["A4H_PASSWORD"]}'
self.auth = "Basic " + base64.b64encode(auth.encode()).decode()
self.opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
self.csrf = None
def _req(self, method, path, body=None, headers=None):
url = f"{self.base}{path}{'&' if '?' in path else '?'}sap-client={self.client}"
h = {"Authorization": self.auth}
if self.csrf:
h["x-csrf-token"] = self.csrf
h.update(headers or {})
req = urllib.request.Request(url, body.encode() if body else None, h, method=method)
with self.opener.open(req, timeout=300) as r:
return r.status, dict(r.headers), r.read().decode()
def fetch_csrf(self):
_, hdr, _ = self._req("GET", "/sap/bc/adt/discovery",
headers={"x-csrf-token": "fetch", "Accept": "*/*"})
self.csrf = hdr.get("x-csrf-token") or hdr.get("X-CSRF-Token")
def delete(self, uris):
"""Delete objects by ADT URI. Returns {uri: (deleted, message)}."""
if not uris:
return {}
if not self.csrf:
self.fetch_csrf()
objs = "".join(f""
for u in uris)
body = (''
'' + objs + '')
_, _, text = self._req("POST", "/sap/bc/adt/deletion/delete", body,
{"Content-Type": DEL_REQ, "Accept": DEL_RES})
out = {}
for m in re.finditer(r']*)>(.*?)', text, re.S):
attrs, inner = m.group(1), m.group(2)
uri = re.search(r'adtcore:uri="([^"]+)"', attrs)
ok = re.search(r'del:isDeleted="([^"]+)"', attrs)
msg = re.search(r'(.*?)', inner, re.S)
out[uri.group(1) if uri else "?"] = (ok is not None and ok.group(1) == "true",
msg.group(1) if msg else "")
return out