Metadata-Version: 2.1
Name: wise-aiops
Version: 0.0.2.post1
Summary: AIops와 동일한 저장 규칙(MySQL 메타DB/스토리지/MLflow)을 쓰는 독립 파이썬 패키지
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: pandas ==2.1.4
Requires-Dist: numpy ==1.26.4
Requires-Dist: pyarrow >=19.0.0
Requires-Dist: sqlalchemy <2,>=1.4
Requires-Dist: PyMySQL <2,>=1.0
Requires-Dist: xgboost ==2.1.1
Requires-Dist: mlflow ==3.4.0
Requires-Dist: scikit-learn ==1.7.2
Requires-Dist: scikit-learn-extra ==0.3.0
Requires-Dist: protobuf <=5.29.0
Requires-Dist: pycryptodome >=3.19
Requires-Dist: hdfs <3,>=2.7
Requires-Dist: requests >=2.26
Requires-Dist: joblib
Requires-Dist: onnx ==1.22.0
Requires-Dist: onnxruntime ==1.23.2
Requires-Dist: skl2onnx ==1.20.0
Provides-Extra: all-frameworks
Requires-Dist: torch ; extra == 'all-frameworks'
Requires-Dist: tensorflow ; extra == 'all-frameworks'
Requires-Dist: onnx ==1.22.0 ; extra == 'all-frameworks'
Requires-Dist: onnxruntime ; extra == 'all-frameworks'
Requires-Dist: transformers ; extra == 'all-frameworks'
Provides-Extra: notebook
Requires-Dist: jupyter ; extra == 'notebook'
Requires-Dist: ipykernel ; extra == 'notebook'
Provides-Extra: pytorch
Requires-Dist: torch ; extra == 'pytorch'
Provides-Extra: tensorflow
Requires-Dist: tensorflow ; extra == 'tensorflow'
Provides-Extra: transformers
Requires-Dist: transformers ; extra == 'transformers'

# wise-aiops

WP-Platform(`wp-ml`)이 쓰는 것과 **동일한 저장 규칙**(MySQL 메타DB `WISE_PROPHET_V1`, 데이터셋
파일 경로, MLflow 트래킹/아티팩트 경로)을 따르는 독립 파이썬 패키지다. **JupyterHub/notebook에서도
wp-platform 저장소에 모델과 데이터셋을 저장할 수 있도록** wheel로 빌드해 pip install해서 쓰도록
설계되었다.

## 빌드 및 설치

빌드

```bash
cd projects/wise-aiops
pip install build
python -m build          # dist/wise_aiops-0.1.0-py3-none-any.whl 생성
```

설치

```bash
pip install wise_aiops
```

인터넷 없을 때 설치

```bash
pip install wise_aiops-0.1.0-py3-none-any.whl
```

<!--
**MLflow 버전 주의**: MLflow는 트래킹 스토어(DB) 스키마가 버전에 민감하다. `pyproject.toml`은
`mlflow>=2.9`로 넓게 잡아두었지만, **실제 wp-ml이 구동 중인 MLflow 버전을 `pip show mlflow`로
확인해 정확히 맞춰 설치해야 한다.** 버전이 다르면 같은 트래킹 스토어를 읽고 쓸 때 호환 문제가
생길 수 있다.


**프레임워크별 선택 의존성**: 기본 의존성에는 `pandas`/`numpy`/`scikit-learn`/`mlflow`/`sqlalchemy`/
`onnx`(`framework_type='ONNX'` 저장/판별에 항상 필요해 필수로 설치된다) 등 핵심 패키지가
들어있다. `save_model(framework_type=...)`으로 PyTorch/TensorFlow/Transformers 모델을 저장·
불러오거나, ONNX 변환/실행에 `onnxruntime`/`skl2onnx`/`tf2onnx`가 더 필요하면 해당 패키지를
추가로 설치해야 한다(`serviceModel.py`가 `try/except ImportError`로 감지하므로 설치 안 해도
나머지 기능은 그대로 동작):

```bash
pip install "wise_aiops[pytorch]"        # torch
pip install "wise_aiops[tensorflow]"     # tensorflow
pip install "wise_aiops[onnx]"           # onnxruntime, skl2onnx, tf2onnx (onnx 자체는 기본 설치됨)
pip install "wise_aiops[transformers]"   # transformers
pip install "wise_aiops[all-frameworks]" # 위 전부
```
-->

## 환경변수 설정 (필수)

기존에는 `assets/config/app.config.json` 파일을 읽었지만, 이 패키지는 환경변수만으로 동작한다.

| 환경변수 | 설명 |
|---|---|
| `DB_HOST` | META_DB 호스트 |
| `DB_PORT` | META_DB 포트 | 
| `DB_USER` | META_DB 사용자 |
| `DB_PASSWORD` | META_DB 비밀번호 — **RSA로 암호화된 base64 문자열 그대로** (평문 아님) |
| `DB_PRIVATE_KEY` | RSA 개인키 — PEM 텍스트를 그대로 넣거나, 마운트된 키 파일의 경로를 지정(값이 존재하는 파일 경로면 파일로, 아니면 PEM 원문으로 자동 판별) | 예 |
| `DB_NAME` | META_DB 스키마명 |
| `DS_ID` | 데이터셋/모델 저장에 쓸 `DS_MSTR.DS_ID` | 
| `FILE_FORMAT` | 데이터셋 파일 포맷 |
| `JUPYTER_HUB_USER_NO` | 플랫폼 숫자 `USER_NO` (JupyterHub 스포너가 사용자별로 주입) |
| `PIP_DIR` | `servicePackage.install_package()`가 쓸 오프라인 패키지 디렉터리(로컬 wheel/sdist 모음). `local_dir` 인자를 생략했을 때만 사용됨 |

`JupyterHub Spawner` 설정에서 사용자별 `JUPYTER_HUB_USER_NO`, 그리고 나머지 `DB_*`/`DS_ID`/
`FILE_FORMAT` 값들을 환경변수로 설정하여 구성한다.
<!--
## 사용 예시 (Jupyter 노트북)

```python
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from wise_aiops import WiseAiops

client = WiseAiops()  # user_no 생략 시 JUPYTER_HUB_USER_NO 환경변수를 자동으로 사용



3) 모델 저장 (MLflow + DP_MODEL_MSTR/DP_MODEL_CUSTOM_MSTR 등록, 모델관리 UI에서도 조회 가능)
model = RandomForestClassifier().fit(loaded_df[["x1", "x2"]], loaded_df["y"])
result = client.save_model(
    model,
    arg_type="Classification",          # 필수 — "Classification" | "Regression" | "Clustering" 중 하나
    model_name="aiops-sample-model",   # 생략하면 type(model).__name__으로 자동 유도
    # framework_type 생략 시 model 객체를 보고 자동 판별(Scikit-learn/PyTorch/TensorFlow/ONNX/Transformers)
    sample_data=loaded_df[["x1", "x2"]].head(),
    metrics={"accuracy": 0.93},
)
print(result)  # {'model_id':.., 'model_idx':.., 'run_id':.., 'mlflow_path':..}

같은 model_name으로 다시 save_model을 호출하면 새 모델이 아니라 같은 모델의 새 버전으로 저장된다.

4) 모델 목록 / 모델 파일 + scaler 함께 불러오기 (숫자 MODEL_ID 또는 문자열 model_name 둘 다 사용 가능)
print(client.list_models())
loaded_model, scaler = client.load_model_file("aiops-sample-model")  # 또는 result["model_id"]
scaler 없이 저장했다면 scaler는 None

sample_data를 넘겨 저장했다면, 불러온 모델에서 학습 데이터의 컬럼 스키마를 다시 조회할 수 있다.
print(loaded_model.metadata.get_input_schema())

Classification 모델이고 model.classes_가 있었다면, MLflow run의 'encoder' 태그에 라벨 값이
JSON으로 남는다(client.get_run_info(run_id).data.tags['encoder']).
```

`model_obj`로 sklearn `Pipeline`(예: `Pipeline([("scaler", StandardScaler()), ("clf", ...)])`)을
그대로 넘기면, 분류기/회귀기/군집기 스텝과 스케일러 스텝을 자동으로 분리해 각각 모델/스케일러로
저장한다(직접 `scaler=...`를 넘기면 그 값이 우선한다).

서브모듈을 직접 쓸 수도 있다(내부적으로 `user_no`/`ds_id`를 매 호출마다 인자로 받는다) — 기능별로
`serviceData`(데이터셋)/`serviceModel`(모델)/`servicePackage`(pip)로 나뉘어 있으며, wp-ml의
`serviceModel/`/`serviceData/` 명명 관례를 따른 것이다:

```python
from wise_aiops import serviceData, serviceModel

serviceData.save_dataset("aiops_sample_dataset", df, user_no=1000)
serviceModel.save_model(model, arg_type="Classification", model_name="aiops-sample-model", user_no=1000)
```


## 패키지 설치 / 목록 조회 / 삭제 (`servicePackage`)

`client.install_package(...)`/`client.list_packages()`/`client.uninstall_package(...)`로
`WiseAiops` 인스턴스에서도 동일하게 호출할 수 있다(단, user_no/ds_id와 무관한 동작이라 모듈
함수를 그대로 위임한다). 내부적으로 `python -m pip install`/`list`/`uninstall -y`를 서브프로세스로
실행하며, 실패 시(0이 아닌 종료 코드) `RuntimeError`로 stdout/stderr를 포함해 예외를 던진다.


## 알려진 제약 (인프라/구현 범위)

- **LOCAL 스토리지 + 마운트 공유 필요**: `DS_MSTR.TYPE`이 `LOCAL`이면, wise-aiops가 저장한
  파일이 wp-ml과 실제로 같은 위치에 있으려면 그 `DEFAULT_PATH`가 JupyterHub 컨테이너/서버에도
  **마운트되어 공유**되어야 한다. `HDFS`는 네트워크로 접근하므로 이 문제가 없다.
  
- **scaler는 로컬 파일시스템 접근을 전제로 한다**: `joblib.dump`/`joblib.load`를 모델 아티팩트
  경로에 직접 수행한다. HDFS 아티팩트 스토어에서는 원래 wp-ml 자체도 이 방식만 지원하므로
  동일한 한계를 그대로 물려받는 것이지 새로 생기는 문제가 아니다.
  
- **데이터셋 등록은 최소 범위만 재현한다**: wp-server(Node)의 `/wd/addDataset` 전체 파이프라인
  (`WpDataSourceManager`, Spark 기반 컬럼 통계 트리거 등)까지 재현하지는 않는다.
  `DS_VIEW_MSTR`/`DS_VIEW_TBL_MSTR`/`DS_VIEW_HISTORY`에 유효한 행을 만들어 data-manager UI
  목록에 노출되는 것과, wise-aiops 자체의 save/read/list 일관성 확보까지가 범위다. 컬럼
  프로파일링 통계(`DS_VIEW_COL_MSTR`)는 채우지 않는다.
  
- **`list_models`는 wp-ml의 `getModelList`를 그대로 재현하지 않는다**: 그 쿼리는
  `DP_MODEL_DATASET_USE_MSTR`를 INNER JOIN하므로, wise-aiops로 저장한 모델처럼 그 테이블에
  사용 기록이 없는 모델은 조회에서 빠진다. 대신 `DP_MODEL_MSTR`(+`DP_ARG_MSTR`) 기준의 단순한
  자체 쿼리를 쓴다.
  
- **모델 식별자는 `model_name`혹은 `model_id`를 사용한다**: `projects/wp-ml/job/common/
  model-custom.py` → `serviceModel/analyticJobService.py::saveCustomModel`을 참조해, 같은
  `model_name`(+`user_no`)으로 다시 저장하면 새 `MODEL_ID`가 아니라 같은 모델의 새 버전
  (`MODEL_IDX`)이 된다. `DP_MODEL_MSTR.ARG_ID`도 이 참조 구현과 동일하게 쓰지 않는다(컬럼이
  nullable — 커스텀 모델은 원래 비워둠).
  
- **`arg_type`은 필수 인자다**: `serviceModel.save_model(..., arg_type=...)`에 `"Classification"` /
  `"Regression"` / `"Clustering"` 중 하나를 반드시 지정해야 한다(그 외 값은 `ValueError`).
  `DP_MODEL_MSTR.ARG_TYPE`과 `DP_MODEL_CUSTOM_MSTR.ARG_TYPE`에 그대로 반영된다.
  
- **PyTorch/TensorFlow-Keras는 `config.json`도 함께 남긴다**: 모델 아티팩트 경로에
  `evaluation`(레이어별 이름/타입/input-output shape/파라미터 수 — `analyticJobService.
  getLayerFromTfModel`/`getLayerFromPytorchModel`과 동일한 방식)과 `feature_importance`(전체/
  학습가능/고정 파라미터 수 요약 — `getParamFromTfModel`/`getParamFromPytorchModel`과 동일한
  방식)를 담아 `client.log_config`로 저장한다. 다른 프레임워크(Scikit-learn/ONNX/Transformers)는
  해당 사항 없음(`saveCustomModel`도 이 두 프레임워크에만 `config['evaluation']`을 채움).
  
- **`load_model_file`은 (model, scaler) 튜플을 반환한다**: scaler를 저장하지 않았다면 두 번째
  값은 `None`이다. 모델만 필요하면 `model, _ = client.load_model_file(...)`처럼 받으면 된다.
  
- **`sample_data`를 저장하면 MLflow signature로 컬럼 스키마가 남는다**: `load_model_file`로 불러온
  모델의 `model.metadata.get_input_schema()`로 학습 시 컬럼명/타입을 다시 조회할 수 있다.
  `sample_data`는 `None`이 아니면 반드시 `pandas.DataFrame`이어야 한다 — 리스트/ndarray 등 다른
  타입을 넘기면 `ValueError`가 난다(컬럼명이 있어야 signature/`get_input_schema()`가 의미 있음).
  
- **Classification 모델은 라벨 값이 `encoder` 태그로 남는다**: `arg_type="Classification"`으로
  저장하고 모델에 `classes_` 속성이 있으면(sklearn 분류기), 그 목표 변수 라벨 값이 MLflow run의
  `encoder` 태그에 JSON으로 기록된다. `save_model(label_encoder=...)`로 `sklearn.preprocessing.
  LabelEncoder` 같은 인코더 객체나 라벨 값 리스트를 직접 넘기면, 모델의 `classes_` 대신 그 값이
  우선 기록된다(모델 자체에 `classes_`가 없는 경우에도 라벨을 남길 수 있다).
  
- **`load_model_file`/`load_scaler_file`/`read_dataset`은 숫자(ID) 또는 문자열(이름) 모두 받는다**:
  정수를 넘기면 그 값을 `MODEL_ID`/`DS_VIEW_ID`로 바로 쓰고, 문자열을 넘기면 이름
  (`MODEL_NM`/`DS_VIEW_NM`, +`user_no`)으로 조회해 ID로 변환한다.
  
- **`list_models`는 `model_type`으로 일반/커스텀 모델을 구분해 조회할 수 있다*
*: 생략(`None`)하면
  전체, `"normal"`이면 커스텀 모델이 아닌 것만(`CUSTOM_YN != 'Y'`), `"custom"`이면 커스텀 모델만
  (`CUSTOM_YN = 'Y'`) 조회한다.
  
- **`list_models`의 `model_id`도 숫자 또는 문자열을 모두 받는다**: 숫자(또는 숫자로만 이루어진
  문자열)면 `MODEL_ID` 기준으로, 그 외 문자열이면 `MODEL_NM` 기준으로 조회한다.
  
- **`list_models`는 `version`으로 특정 버전만 조회할 수 있다**: 생략(`None`)하면 모델별 가장
  마지막에 저장된 버전(`MAX(MODEL_IDX)`)만 나온다. 숫자를 넘기면 그 `MODEL_IDX` 버전만 조회하고,
  그 조건에 맞는 버전이 없으면 `ValueError`가 난다. 숫자가 아니면 역시 `ValueError`.
  
- **ONNX로 저장할 때 원본이 TF/PyTorch였다면 `source_model`을 넘겨 레이어 정보를 살릴 수 있다**:
  `framework_type='ONNX'`로 저장하면 `model_obj`는 이미 `onnx.ModelProto`라 레이어 구조 정보가
  없다. 변환 전 원본 TensorFlow/Keras 또는 PyTorch 모델 객체를 `source_model=...`로 함께 넘기면,
  그 원본에서 위와 동일한 `evaluation`/`feature_importance`를 추출해 `config.json`에 담는다
  (예: `serviceModel.save_model(onnx_model, arg_type="Classification",
  framework_type="ONNX", source_model=original_pytorch_model)`). 생략하거나 원본이 TF/PyTorch가
  아니면 ONNX 저장 시 `config.json`은 남기지 않는다.
-->

## 기능 설명
<!--
각 함수가 하는 일과 파라미터를 간단히 정리한다. `WiseAiops` 파사드(`client.xxx(...)`)로 쓰면
`user_no`/`ds_id`가 인스턴스 생성 시 값으로 자동 채워지고, `serviceData`/`serviceModel`/
`servicePackage` 모듈 함수를 직접 쓰면 그 값을 매 호출마다 인자로 넘긴다. 아래 시그니처는 모듈
함수 기준이다.
-->
### `serviceData` : 데이터셋 저장, 불러오기, 목록 조회 기능

- **`save_dataset(dataset_name, dataframe, user_no=None, ds_id=None)`** : 데이터셋 저장 가능. 
  `DS_VIEW_MSTR`/`DS_VIEW_TBL_MSTR`/`DS_VIEW_HISTORY`에도 등록되어 data-manager UI
  목록에서 조회 가능하다. 같은 `dataset_name`으로 다시 저장하면 새 버전(`VIEW_IDX`)으로 쌓인다.
  - `dataset_name`: 데이터셋 이름(`DS_VIEW_MSTR.DS_VIEW_NM`)
  - `dataframe`: 저장할 `pandas.DataFrame`
  - `user_no`: 생략 시 `JUPYTER_HUB_USER_NO` 환경변수
  - `ds_id`: 저장소 연결 ID(`DS_MSTR.DS_ID`). 생략 시 `DS_ID` 환경변수
  - ex) `serviceData.save_dataset("aiops_sample_dataset", df, user_no=1000)`

- **`read_dataset(dataset, version=None, user_no=None, ds_id=None)`** : 저장된 데이터셋을
  `pandas.DataFrame`으로 읽어온다.
  - `dataset`: 숫자(`DS_VIEW_ID`) 또는 문자열(`DS_VIEW_NM`) 둘 다 가능
  - `version`: 생략 시 최신 버전
  - ex) `serviceData.read_dataset("aiops_sample_dataset")` 또는 `serviceData.read_dataset(12, version=2)`

- **`list_datasets(user_no=None)`** : 사용자가 소유한 데이터셋 목록을 조회한다(스토리지 접근
  없이 메타DB만 조회).
  - `user_no`: 생략 시 `JUPYTER_HUB_USER_NO` 환경변수
  - ex) `print(serviceData.list_datasets())`

### `serviceModel` : 모델 저장, 불러오기, 목록 조회 기능

- **`save_model(model_obj, arg_type, model_name, *, framework_type=None, scaler=None, sample_data=None, metrics=None, params=None, target=None, label_encoder=None, source_model=None, user_no=None, ds_id=None)`** :
  학습된 모델을 MLflow에 저장하고 `DP_MODEL_MSTR`/`DP_MODEL_CUSTOM_MSTR`에 등록한다.
  - `model_obj`: 학습된 모델 객체, — 필수
  - `arg_type`: 알고리즘의 종류, 필수 — `"Classification"` | `"Regression"` | `"Clustering"` 이 있음
  - `model_name`: 모델 이름, 필수 — 같은 이름으로 다시 저장하면 새 모델이 아니라 새 버전(`MODEL_IDX`)으로 저장됨
  - `framework_type`: 모델 프레임워크 종류. 'Scikit-learn', 'PyTorch', 'ONNX' 총 3종류가 지원되며 생략 시 `model_obj` 타입을 보고 자동 판별
  - `scaler`: 스케일러 객체
  - `sample_data`: 학습 데이터 예시(`pandas.DataFrame`이어야 함) — MLflow signature로 컬럼 스키마가 남아 나중에 `get_input_schema()`로 조회 가능
  - `label_encoder`: Classification일 때 `classes_`를 가진 인코더 또는 라벨 값 리스트를 직접 지정
  - ex) 파라미터 전부 다 쓸 경우 `serviceModel.save_model(my_model, 'Classification', 'my_model_name', scaler = scaler, framework_type = 'Scikit-learn',sample_data=pd.DataFrame(X_train_scaled, columns=X.columns), label_encoder=targetEncoder.classes_)`
  - ex) `serviceModel.save_model(my_model, 'Classification', 'my_model_name')`

- **`load_model_file(model, model_idx=None, framework_type=None, user_no=None, ds_id=None)`** : 저장된 모델과 스케일러를 불러오는 기능
  MLflow에 저장된 모델과 scaler를 함께 불러온다.
  - `model`: 숫자(`MODEL_ID`) 또는 문자열(`MODEL_NM`)로 모델을 검색
  - `model_idx`: 생략 시 최신 버전, 숫자면 
  - `framework_type`: 생략 시 `mlflow.pyfunc.load_model` 사용
  - 반환값: `(model, scaler)' — scaler를 저장하지 않았다면 scaler는 `None` 값을 반환
  - ex) `model, scaler = serviceModel.load_model_file("my-model")`

- **`list_models(model_id="", model_type=None, user_no=None)`** : 모델 목록을 조회하는 기능.
  - `model_id`: 값이 없으면 전체 목록. 숫자면 `MODEL_ID`, 문자열이면 `MODEL_NM` 기준으로 검색
  - `model_type`: `None`이면 전체, `"normal"`이면 커스텀 아닌 것만, `"custom"`이면 커스텀 모델만 목록으로 불러옴
  - ex) `serviceModel.list_models()` — 전체 모델 리스트 불러오기
  - ex) `serviceModel.list_models(model_id="my-model", model_type="custom")` - my-model 이 이름인 모델을 커스텀 모델 중에서 불러오기

### `servicePackage` : pip 패키지 설치, 목록 조회, 삭제 기능

- **`install_package(package, local_dir=None, upgrade=False, extra_args=None)`** : 인터넷 또는 오프라인 로컬 디렉터리에 있는 패키지를 설치하는 기능.
  - `package`: 패키지명
  - `local_dir`: 오프라인 설치 경로. 생략하면 `PIP_DIR` 환경변수, 그것도 없으면 인터넷(PyPI)에서 패키지명을 찾아 다운로드
  - `upgrade`: `True`면 `--upgrade` 추가
  - `extra_args`: `pip install`에 추가로 넘길 인자 리스트
  - ex) `servicePackage.install_package("lightgbm==4.1.0")`

- **`list_packages()`** : 현재 파이썬 환경에 설치된 pip 패키지 목록을 조회하는 기능
  - ex) `print(servicePackage.list_packages())`

- **`uninstall_package(package, extra_args=None)`** : pip으로 패키지를 삭제하는 기능.
  - `package`: 패키지명 또는 리스트
  - `extra_args`: `pip uninstall`에 추가로 넘길 인자 리스트
  - ex) `servicePackage.uninstall_package("lightgbm")`

인터넷이 되는 환경에서는 PyPI에서 바로 설치하고, 인터넷이 차단된 환경에서는 로컬 디렉터리(오프라인
wheel/sdist 모음)에서 설치할 수 있다. 로컬 디렉터리는 `local_dir` 인자로 넘기거나, 생략하면
`PIP_DIR` 환경변수를 확인한다 — 우선순위는 `local_dir` 인자 > `PIP_DIR` 환경변수 > (둘 다 없으면)
인터넷(PyPI) 설치다.

```

1) 인터넷에서 설치 (local_dir 미지정, PIP_DIR도 없을 때)
servicePackage.install_package("lightgbm==4.1.0")

2) 로컬 디렉터리에서 오프라인 설치 (인자로 직접 경로 지정)
servicePackage.install_package("lightgbm", local_dir="/opt/offline-packages")

3) PIP_DIR 환경변수로 오프라인 경로 지정 (local_dir 인자 생략)
   os.environ["PIP_DIR"] = "/opt/offline-packages" 가 이미 설정되어 있다고 가정
servicePackage.install_package("lightgbm")

여러 패키지를 한 번에
servicePackage.install_package(["lightgbm", "catboost==1.2"])

4) 설치된 패키지 목록 조회 (pandas.DataFrame, columns: name, version)
print(servicePackage.list_packages())

5) 패키지 삭제 (역시 문자열 또는 리스트)
servicePackage.uninstall_package("lightgbm")
```

## 패키지 구조

```
wise_aiops/
  __init__.py           # 공개 API export (serviceData/serviceModel/servicePackage/WiseAiops)
  _settings.py          # 환경변수 로딩
  _crypto.py            # 비밀번호 처리
  _db.py                # MetaDb — MySQL 접속/조회/삽입/갱신 최소 DAO
  _storage.py           # LOCAL/HDFS 파일 read/write 디스패치
  _mlflow_client.py     # MLflow 트래킹/아티팩트 클라이언트
  serviceData.py        # save_dataset / read_dataset / list_datasets
  serviceModel.py       # save_model / list_models / load_model_file / load_scaler_file
  servicePackage.py     # install_package / list_packages / uninstall_package
  client.py             # WiseAiops 파사드 클래스
```

<!--
## 참고 문서

`wise_aiops/serviceModel.py`(참고로 wp-ml에도 동명의 `projects/wp-ml/serviceModel/` 디렉터리가
있으나 서로 다른 코드다 — wise-aiops는 import하지 않는다)의 모델 저장/불러오기 로직은
`projects/wp-ml/job/common/model-custom.py` → `serviceModel/analyticJobService.py::
saveCustomModel`(커스텀 모델 업로드/등록 파이프라인)을 참조 구현으로 삼았다 — 코드를
import하지 않고, 같은 저장 방식(`MODEL_NM` 기준 식별, MLflow 태그 세트, sklearn `Pipeline`
자동분해)를 표준 패키지로 재구현했다.
-->
