from_sap: 펑션 그룹(펑션 모듈 소스)도 받는다 — SAPL<그룹> + INCLUDE 문 추적
- GET_PROGRAM_LIST 는 TADIR 의 PROG 만 주므로 GET_OBJECT_LIST_BY_PACKAGE(FUGR) 로 그룹을 받아 SAPL<그룹> 을 프로그램처럼 저장 (설명에 펑션 이름·텍스트 목록) - 펑션 그룹은 서버 INCLUDE_LIST 가 비어 온다(실측) → 소스의 INCLUDE 문을 깊이 4 까지 따라가며 고객 인클루드(Z/Y/네임스페이스, L<그룹>…)만 받는다. 표준 생성기 인클루드(LSVIM…)는 제외 - 실측 ZFI01: 펑션 그룹 14, SAPLZFG_FI01 인클루드 47 → 파서 FUNCTION 38 unit, 미인식 0 - --no-fugr 로 끌 수 있다 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5df8595fe2
commit
fdae34161e
+103
-3
@@ -19,6 +19,9 @@ ZAA_ICF 서비스(ZCL_ZAA_AGENT_API)가 설치돼 있어야 한다.
|
||||
INCLUDE_PROGRAM: [{INCLUDE, SOURCE_CODE}], TCODE_LIST: [...]}
|
||||
_errors.log 실패한 프로그램 (계속 진행하고 여기 기록)
|
||||
|
||||
- 펑션 모듈: 프로그램 목록 API 에는 안 나온다(TADIR 의 FUGR). 패키지의 펑션 그룹을 따로 받아
|
||||
그룹 메인 프로그램 SAPL<그룹> 을 프로그램처럼 저장한다 (펑션 소스는 그 인클루드 L<그룹>U01 …).
|
||||
펑션 그룹은 서버 INCLUDE_LIST 가 비어 오므로 소스의 INCLUDE 문을 따라 받는다. --no-fugr 로 끈다.
|
||||
- SAP 목록 API 는 SUBC 를 생략하면 실행 프로그램(1)과 모듈 풀(M)만 준다. Include 는 각 프로그램의
|
||||
INCLUDE_LIST 로 따라오므로 따로 받지 않는다.
|
||||
- 텍스트 심볼(TEXT-nnn)은 이 API 에 없다 → TEXT_SYMBOL 은 빈 배열 (from_dir 와 같은 한계).
|
||||
@@ -28,6 +31,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -69,6 +73,56 @@ def list_programs(package: str | None = None, pattern: str | None = None,
|
||||
return [r for r in rows if isinstance(r, dict) and r.get("OBJ_NAME")]
|
||||
|
||||
|
||||
# 소스 안의 INCLUDE 문. 주석줄(* 로 시작)은 제외한다
|
||||
_INCLUDE_STMT = re.compile(r"^(?![ \t]*\*)[ \t]*INCLUDE[ \t]+([A-Za-z0-9_/]+)[ \t]*\.", re.I | re.M)
|
||||
MAX_INCLUDE_DEPTH = 4
|
||||
|
||||
|
||||
def is_customer_include(name: str, program: str) -> bool:
|
||||
"""받아올 인클루드인가 — 고객 개발(Z/Y/네임스페이스)과 그 펑션 그룹의 L<그룹>… 인클루드만.
|
||||
|
||||
SAP 표준(LSVIMFXX, lsvimdat 같은 생성기 인클루드)은 소스가 크고 업무 로직이 아니라 뺀다.
|
||||
"""
|
||||
n = name.upper()
|
||||
if n.startswith(("Z", "Y", "/")):
|
||||
return True
|
||||
p = program.upper()
|
||||
if p.startswith("SAPL") and n.startswith("L" + p[4:]): # SAPLZFG_FI01 → LZFG_FI01TOP / U01 / F01 …
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_includes_from_source(program: str, includes: list[dict], seen: set[str]) -> int:
|
||||
"""INCLUDE 문을 따라 인클루드를 받아 includes 에 덧붙인다 (D010INC 가 비어 있을 때의 대안).
|
||||
|
||||
펑션 그룹(SAPL*)은 서버의 INCLUDE_LIST 가 비어 온다(실측 2026-09-22). 메인 → LxxxUXX → LxxxU01 …
|
||||
처럼 단계가 있어 깊이 MAX_INCLUDE_DEPTH 까지 따라간다. 받은 개수를 돌려준다.
|
||||
"""
|
||||
added = 0
|
||||
frontier = list(includes)
|
||||
for _ in range(MAX_INCLUDE_DEPTH):
|
||||
next_frontier: list[dict] = []
|
||||
for inc in frontier:
|
||||
for ref in _INCLUDE_STMT.findall(inc["SOURCE_CODE"]):
|
||||
ref = ref.upper()
|
||||
if ref in seen or not is_customer_include(ref, program):
|
||||
continue
|
||||
seen.add(ref)
|
||||
res = _call("GET_PROGRAM_SOURCE", {"IV_PROGRAM": ref})
|
||||
code = ((res.get("parsed") or {}).get("SOURCE_CODE") or "") if not res.get("error") else ""
|
||||
if res.get("error") or res["return"]["type"] == "E" or not code.strip():
|
||||
_log(f" include {ref} 못 받음: {res.get('error') or res['return']['message']}")
|
||||
continue
|
||||
item = {"INCLUDE": ref, "SOURCE_CODE": code}
|
||||
includes.append(item)
|
||||
next_frontier.append(item)
|
||||
added += 1
|
||||
if not next_frontier:
|
||||
break
|
||||
frontier = next_frontier
|
||||
return added
|
||||
|
||||
|
||||
def fetch_program(name: str, description: str = "") -> dict:
|
||||
"""GET_PROGRAM_SOURCE(+Include) → 수집 JSON 모양의 payload. 실패하면 RuntimeError."""
|
||||
res = _call("GET_PROGRAM_SOURCE", {"IV_PROGRAM": name, "IV_WITH_INCLUDE": "X"})
|
||||
@@ -95,6 +149,8 @@ def fetch_program(name: str, description: str = "") -> dict:
|
||||
continue
|
||||
seen.add(inc_name)
|
||||
includes.append({"INCLUDE": inc_name, "SOURCE_CODE": code})
|
||||
# 서버 INCLUDE_LIST 에 없는 인클루드를 소스의 INCLUDE 문으로 보충 (펑션 그룹은 이 경로로만 받아진다)
|
||||
_resolve_includes_from_source(program, includes, seen)
|
||||
|
||||
tcodes = [
|
||||
{"TCODE": (t.get("TCODE") or "").strip().upper(), "TTEXT": t.get("TTEXT") or ""}
|
||||
@@ -109,6 +165,35 @@ def fetch_program(name: str, description: str = "") -> dict:
|
||||
}
|
||||
|
||||
|
||||
def list_function_groups(package: str) -> list[dict]:
|
||||
"""패키지의 펑션 그룹 → [{OBJ_NAME: 'SAPLZFG_FI01', TEXT: '펑션 그룹 ZFG_FI01: FN1, FN2 …'}]
|
||||
|
||||
펑션 모듈 소스는 그룹 메인 프로그램 SAPL<그룹> 의 인클루드(L<그룹>U01 …)에 있다.
|
||||
GET_PROGRAM_LIST 는 TADIR 의 PROG 만 주므로 펑션 그룹(FUGR)은 여기서 따로 받는다.
|
||||
"""
|
||||
res = _call("GET_OBJECT_LIST_BY_PACKAGE", {"IV_PACKAGE": package, "IV_OBJ_TYPE": "FUGR"})
|
||||
if res.get("error"):
|
||||
raise RuntimeError(res["error"])
|
||||
if res["return"]["type"] == "E":
|
||||
raise RuntimeError(f"SAP 오류: {res['return']['message']}")
|
||||
out = []
|
||||
for r in (res.get("parsed") or {}).get("RESULT") or []:
|
||||
if not isinstance(r, dict) or not r.get("OBJ_NAME"):
|
||||
continue
|
||||
group = r["OBJ_NAME"].strip().upper()
|
||||
# 그룹 안의 펑션 이름·설명을 프로그램 설명으로 — 펑션 이름 검색이 프로그램 단위에서도 되게
|
||||
names: list[str] = []
|
||||
fl = _call("GET_FUNCTION_LIST", {"IV_FUNC_GROUP": group})
|
||||
if not fl.get("error"):
|
||||
for f in (fl.get("parsed") or {}).get("RESULT") or []:
|
||||
if isinstance(f, dict) and f.get("FUNCNAME"):
|
||||
text = (f.get("STEXT") or f.get("FUNC_TEXT") or "").strip()
|
||||
names.append(f["FUNCNAME"].strip().upper() + (f" ({text})" if text else ""))
|
||||
desc = f"펑션 그룹 {group}" + (": " + ", ".join(names[:30]) if names else "")
|
||||
out.append({"OBJ_NAME": "SAPL" + group, "TEXT": desc, "SUBC": "F", "UDAT": r.get("UDAT") or ""})
|
||||
return out
|
||||
|
||||
|
||||
def package_rows(package: str, rows: list[dict]) -> list[dict]:
|
||||
"""프로그램 목록 → normalize 가 packages.jsonl 로 만드는 package_list 모양."""
|
||||
return [
|
||||
@@ -129,9 +214,11 @@ def _write_json(path: Path, obj) -> None:
|
||||
|
||||
|
||||
def run(packages: list[str], out_dir: Path, pattern: str | None = None, subc: str | None = None,
|
||||
limit: int | None = None, force: bool = False, dry_run: bool = False) -> dict:
|
||||
limit: int | None = None, force: bool = False, dry_run: bool = False,
|
||||
fugr: bool = True) -> dict:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
stats = {"packages": 0, "listed": 0, "fetched": 0, "skipped": 0, "errors": 0, "includes": 0}
|
||||
stats = {"packages": 0, "listed": 0, "function_groups": 0, "fetched": 0, "skipped": 0, "errors": 0,
|
||||
"includes": 0}
|
||||
errors: list[str] = []
|
||||
|
||||
targets: list[tuple[str, str]] = [] # (program, description)
|
||||
@@ -150,6 +237,17 @@ def run(packages: list[str], out_dir: Path, pattern: str | None = None, subc: st
|
||||
continue
|
||||
stats["packages"] += 1
|
||||
_log(f" 프로그램 {len(rows)}건")
|
||||
if fugr and not pattern:
|
||||
# 펑션 그룹 → SAPL<그룹> 을 프로그램처럼 받는다 (펑션 모듈 소스가 그 인클루드에 있다)
|
||||
try:
|
||||
groups = list_function_groups(pkg)
|
||||
_log(f" 펑션 그룹 {len(groups)}건")
|
||||
stats["function_groups"] += len(groups)
|
||||
rows = rows + groups
|
||||
except Exception as e: # noqa: BLE001
|
||||
stats["errors"] += 1
|
||||
errors.append(f"{pkg}\tFUGR_LIST\t{e}")
|
||||
_log(f" ! 펑션 그룹 목록 실패: {e}")
|
||||
if not dry_run:
|
||||
_write_json(out_dir / f"{pkg}.txt", package_rows(pkg, rows))
|
||||
targets += [(r["OBJ_NAME"].upper(), r.get("TEXT") or "") for r in rows]
|
||||
@@ -206,6 +304,8 @@ def main() -> None:
|
||||
ap.add_argument("--limit", type=int, default=None, help="받을 프로그램 수 상한 (시험용)")
|
||||
ap.add_argument("--force", action="store_true", help="이미 받은 파일도 다시 받는다")
|
||||
ap.add_argument("--dry-run", action="store_true", help="목록만 보고 저장하지 않는다")
|
||||
ap.add_argument("--no-fugr", action="store_true",
|
||||
help="펑션 그룹(SAPL<그룹>, 펑션 모듈 소스)은 받지 않는다 (기본은 같이 받음)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.packages and not args.pattern:
|
||||
@@ -221,7 +321,7 @@ def main() -> None:
|
||||
|
||||
out_dir = Path(args.out) if args.out else settings.data_raw
|
||||
stats = run(args.packages, out_dir, pattern=args.pattern, subc=args.subc,
|
||||
limit=args.limit, force=args.force, dry_run=args.dry_run)
|
||||
limit=args.limit, force=args.force, dry_run=args.dry_run, fugr=not args.no_fugr)
|
||||
print(json.dumps(stats, ensure_ascii=False))
|
||||
if stats["errors"]:
|
||||
print(f"실패 {stats['errors']}건 → {out_dir / '_errors.log'}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user