sap_client: ZAA_ICF JSON 규약 대응 (2026-09-14 XML→JSON 전환)
- 요청은 INPUT 래퍼 없이 입력 필드만, 응답은 JSON {"RETURN","RESULT"}. 첫 글자로 asXML 구버전도 자동 판별
- 실측: ZMP_ICF 에서 ZFI01 목록 177건, 프로그램 2본(include 10, T코드 2) 수집 → normalize 정상
- sap/README.md 규약 갱신, tests: JSON/XML 파서·요청 본문 검증
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
921b8ce229
commit
e23140b7af
+38
-4
@@ -5,8 +5,10 @@
|
||||
- 접속정보: .env 파일 또는 환경변수 SAP_URL / SAP_USER / SAP_PASS / SAP_VERIFY_SSL
|
||||
|
||||
호출 규약 (실측, README 참고):
|
||||
POST {SAP_URL}/{METHOD}, Content-Type: application/json, 바디 {"INPUT": {"IV_...": ...}}
|
||||
응답 asXML. XML 선언은 utf-16 이지만 실제 UTF-8 → BOM/선언 제거 후 파싱. 소스 안 제어문자(0x0C) 제거.
|
||||
POST {SAP_URL}/{METHOD}, Content-Type: application/json
|
||||
2026-09-14 부터 JSON: 바디는 래퍼 없이 {"IV_PACKAGE": "ZFI01", "IV_MAX_ROWS": 100} (키 대문자, 숫자는 JSON 숫자,
|
||||
플래그는 "X"). 응답도 JSON {"RETURN": {...}, "RESULT": [...]} (TOTAL_ROWS 는 숫자, 빈 목록은 []).
|
||||
구버전(asXML, 바디 {"INPUT": {...}}) 서버도 응답 첫 글자로 판별해 그대로 읽는다.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
@@ -131,9 +133,41 @@ def parse_asxml(text: str) -> dict:
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ SAP 호출
|
||||
def parse_json_response(text: str) -> dict:
|
||||
"""JSON 응답(2026-09-14 이후) → {parsed, return, error}. parse_asxml 과 같은 모양."""
|
||||
out = {"parsed": None, "return": {"type": "?", "message": ""}, "error": None}
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except ValueError as e:
|
||||
out["error"] = f"JSON 파싱 실패: {e}"
|
||||
out["return"] = {"type": "X", "message": out["error"]}
|
||||
return out
|
||||
if isinstance(data, dict) and "RETURN" not in data and "error" in data: # ICF 핸들러 오류 봉투
|
||||
err = data["error"] if isinstance(data["error"], dict) else {"message": data["error"]}
|
||||
out["parsed"] = {"error": err}
|
||||
out["error"] = f"{err.get('code')}: {err.get('message')}"
|
||||
out["return"] = {"type": "X", "message": out["error"]}
|
||||
return out
|
||||
data = coerce_lists(data)
|
||||
out["parsed"] = data
|
||||
ret = data.get("RETURN") if isinstance(data, dict) else None
|
||||
if isinstance(ret, dict):
|
||||
out["return"] = {"type": ret.get("TYPE") or "-", "message": ret.get("MESSAGE") or ""}
|
||||
return out
|
||||
|
||||
|
||||
def parse_response(raw: bytes) -> dict:
|
||||
"""본문 첫 글자로 JSON / asXML 을 가른다 (BOM 은 둘 다 뗀다)."""
|
||||
text = raw.decode("utf-8", errors="replace").lstrip("").lstrip()
|
||||
if text[:1] in ("{", "["):
|
||||
return parse_json_response(text)
|
||||
return parse_asxml(normalize_xml_text(text))
|
||||
|
||||
|
||||
def sap_call(method: str, params: dict, timeout: int = 180) -> dict:
|
||||
url = f"{CONFIG['base_url']}/{method}"
|
||||
body_obj = {"INPUT": {k: v for k, v in params.items() if v not in ("", None)}}
|
||||
# 2026-09-14 JSON 규약: 래퍼 없이 입력 필드만. 빈 값은 보내지 않는다 (빠진 키 = 초기값)
|
||||
body_obj = {k: v for k, v in params.items() if v not in ("", None)}
|
||||
body = json.dumps(body_obj, ensure_ascii=False)
|
||||
auth = "Basic " + base64.b64encode(f"{CONFIG['user']}:{CONFIG['password']}".encode()).decode()
|
||||
req = urllib.request.Request(
|
||||
@@ -164,5 +198,5 @@ def sap_call(method: str, params: dict, timeout: int = 180) -> dict:
|
||||
raw_text = raw.decode("utf-8", errors="replace")
|
||||
result["raw"] = raw_text
|
||||
result["raw_size"] = len(raw)
|
||||
result.update(parse_asxml(normalize_xml(raw)))
|
||||
result.update(parse_response(raw))
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user