Metadata-Version: 2.4
Name: echoss-common
Version: 2.0.0
Summary: echoss AI Bigdata Center 공통 유틸리티 라이브러리 (Logger, JsonLogDict, config_fileformat)
License-Expression: Apache-2.0
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>6.0.2
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# echoss-common

echoss AI Bigdata Center 공통 유틸리티 라이브러리입니다.
로깅(Logger)과 config 파일 포맷 읽기/쓰기 기능을 제공합니다.

## 문서 지도

- [`prior-work.jsonl`](prior-work.jsonl) — 작업원장. 이전 작업의 결론·이미 해본 것·백로그 (조사 착수 전 조회)
- [`docs/`](docs/) — PDCA 계획·설계·분석 문서

## 설치

```bash
pip install echoss-common
```

## 요구사항

- Python >= 3.12
- pyyaml > 6.0.2

## 2.0.0 변경 사항

호출 방식(함수 이름·인자 이름·인자 순서)은 1.2.0 과 같습니다. 아래 네 가지는 **동작이 바뀌는 호환 단절**입니다.

| 바뀐 것 | 1.2.0 | 2.0.0 | 대응 |
|---|---|---|---|
| 기본 로그 파일 | `file_path` 를 주지 않으면 `logs/echoss.log` 에 기록 | 파일을 만들지 않고 콘솔만 | 파일 로그가 필요하면 `file_path` 를 지정 (`modify_loggers_by_prefix` 는 `new_path`) |
| 없는 설정 파일 | `dict_load` 가 오류 로그를 남기고 빈 dict 반환 | `FileNotFoundError` | 경로를 확인하거나 예외를 처리 |
| YAML 저장 키 순서 | 키를 정렬해 저장 | 넣은 순서대로 저장 | — |
| 설정 파일 인코딩 | 운영체제 기본 인코딩(Windows 는 CP949 등) | 항상 UTF-8 로 읽고 씀 | UTF-8 이 아닌 설정 파일은 UTF-8 로 변환 |

그 밖의 변경:

- `import echoss_common` 이 더 이상 파일이나 폴더를 만들지 않습니다(1.2.0 은 import 만으로 `logs/echoss.log` 를 만들어, 쓰기 금지 폴더에서는 import 가 실패했습니다).
- 로그 회전은 Windows 에서 다른 프로세스가 로그 파일을 열고 있어도 기록을 잃지 않습니다(아래 「로그 회전」).
- 지원하지 않는 형식을 읽으면 호출마다 새 빈 dict 를 돌려줍니다(1.2.0 은 같은 dict 객체를 공유했습니다).

---

## 기능

### 1. Logger (`echoss_logger`)

타임드 로테이팅 파일 핸들러와 콘솔 핸들러를 지원하는 로거 유틸리티입니다.

#### 함수 및 클래스 목록

| 이름 | 종류 | 설명 |
|------|------|------|
| `get_logger()` | 함수 | 새 로거 생성 또는 기존 로거 갱신 |
| `get_app_logger()` | 함수 | 동일 인자로 항상 같은 로거를 반환하는 캐시 기반 로거 |
| `use_logger()` | 함수 | 이미 생성된 로거를 서브모듈에서 가져오기 |
| `set_logger_level()` | 함수 | 로거 레벨 변경 |
| `modify_loggers_by_prefix()` | 함수 | 이름 prefix로 여러 로거를 일괄 수정 |
| `JsonLogDict` | 클래스 | 임의 값을 JSON 문자열로 변환하여 로그 출력 |

#### 상수

| 상수 | 값 |
|------|----|
| `LOG_FORMAT_DETAIL` | `[날짜시간] 레벨 모듈.함수.라인 : 메시지` 형식 |

#### 사용 예시

```python
from echoss_common import get_logger, get_app_logger, use_logger, set_logger_level, modify_loggers_by_prefix, LOG_FORMAT_DETAIL, JsonLogDict

# 기본 로거 생성 (콘솔 + 파일 출력)
logger = get_logger(
    logger_name='myapp',
    logger_format=LOG_FORMAT_DETAIL,   # 상세 포맷 사용 (선택)
    file_path='logs/myapp.log',        # 로그 파일 경로 (기본: None — 파일 없이 콘솔만)
    backup_count=7,                    # 보관할 로그 파일 수 (0이면 전부 보관)
    use_console=True,                  # 콘솔 출력 여부
    level='DEBUG'                      # 로그 레벨
)

logger.info("애플리케이션 시작")
logger.debug("디버그 메시지")
logger.error("오류 발생")

# 캐시 기반 싱글턴 로거 (동일 인자로 호출 시 항상 같은 인스턴스 반환)
app_logger = get_app_logger(logger_name='myapp', file_path='logs/myapp.log')

# 서브모듈에서 기존 로거 재사용
logger = use_logger('myapp')
logger.info("서브모듈에서 로거 사용")

# 로거 레벨만 변경
set_logger_level(logger, 'WARNING')

# prefix로 시작하는 모든 로거 일괄 수정
modify_loggers_by_prefix(
    prefix='myapp',
    new_format=LOG_FORMAT_DETAIL,
    new_path='logs/myapp.log',
    level='INFO'
)
```

#### 로그 회전

파일 로그는 매일 자정에 날짜를 붙인 파일(`myapp.log.2026-10-10`)로 회전합니다.

- 같은 파일에 쓰는 로거는 **로거 이름도 같게** 합니다. 이름이 다르면 같은 파일을 핸들러 둘이 열게 됩니다.
- Windows 에서 다른 프로세스가 로그 파일을 열고 있어 이름 바꾸기가 막히면(`PermissionError`), 날짜 파일에 내용을 복사한 뒤 원본을 비우고 계속 씁니다. 날짜 파일이 이미 있으면(다른 프로세스가 먼저 회전) 덮어쓰지 않습니다.
- 알려진 한계: 복사와 비우기 사이에 **다른 프로세스가 같은 파일에 쓴 줄은 사라질 수 있습니다.** 이 틈은 회전 한 번(보통 자정)뿐입니다. 두 프로세스가 같은 파일에 동시에 쓰는 것 자체도 회전과 무관하게 줄을 잃을 수 있으므로 피합니다.

#### JsonLogDict 사용 예시

로그 메시지에 dict, list 등 복잡한 데이터를 JSON 형태로 출력할 때 사용합니다.
`str()` 또는 `repr()` 호출 시 JSON 문자열로 변환되며, JSON 직렬화가 불가능한 객체는 `str()`로 폴백합니다.

```python
from echoss_common import get_logger, JsonLogDict

logger = get_logger('myapp')

# dict → JSON 문자열로 로그 출력
data = {"user": "kim", "age": 20, "active": True}
logger.info("사용자 정보: %s", JsonLogDict(data))
# 출력: 사용자 정보: {"user": "kim", "age": 20, "active": true}

# list, tuple 등도 지원
logger.debug("결과 목록: %s", JsonLogDict([1, 2, 3]))
# 출력: 결과 목록: [1, 2, 3]

# 중첩 구조도 지원
payload = {"tags": ["a", "b"], "meta": {"version": 1}}
logger.info("페이로드: %s", JsonLogDict(payload))
```

#### JSONL 구조화 로그

`file_path`의 확장자가 `.jsonl`이면 각 로그 레코드는 한 줄의 JSON 객체로 저장됩니다.
구조화된 데이터를 유지하려면 `JsonLogDict`를 메시지 자체로 전달해야 합니다.

```python
from echoss_common import get_logger, JsonLogDict

logger = get_logger(
    "myapp",
    file_path="logs/myapp.jsonl",
    use_console=False,
)

logger.info(JsonLogDict({"user": "kim", "action": "login"}))
# {"timestamp": "...", "level": "INFO", "logger_name": "myapp",
#  "message": {"user": "kim", "action": "login"}}
```

다음 방식은 JSONL 행 자체는 유효하지만, `JsonLogDict`가 문자열 템플릿에 먼저 합쳐져 `message`가 문자열이 됩니다.

```python
logger.info("payload: %s", JsonLogDict({"user": "kim"}))
# message 값은 'payload: {"user": "kim"}' 문자열이 됩니다.
```

---

### 2. config_fileformat (`config_fileformat`)

설정 파일(YAML, JSON, XML, Properties)을 딕셔너리로 읽고 쓰는 유틸리티 함수입니다.

#### 함수 목록

| 함수 | 설명 |
|------|------|
| `dict_load()` | 설정 파일을 `dict`로 읽기 |
| `dict_dump()` | `dict`를 설정 파일로 쓰기 |

#### 지원 포맷

| 확장자 | 포맷 |
|--------|------|
| `.yaml`, `.yml` | YAML |
| `.json` | JSON |
| `.xml` | XML 설정 |
| `.properties` | Java Properties |

XML을 읽을 때는 문서의 루트 태그를 포함한 딕셔너리를 반환합니다. XML 속성은
`@name`, 속성 또는 자식 요소와 함께 있는 직접 본문은 `#text`, 같은 이름의
반복 요소는 리스트로 표현합니다. XML을 쓸 때는 `xml_tag`를 지정하거나 딕셔너리에 루트 키가
하나 있어야 합니다.

- 파일은 항상 UTF-8 로 읽고 씁니다(XML 은 문서 선언을 따릅니다).
- 없는 파일을 읽으면 `FileNotFoundError` 가 납니다. 지원하지 않는 형식은 파일을 열지 않고 오류 로그와 함께 빈 dict 를 돌려줍니다.
- JSON 파일의 최상위가 배열이면 `list` 를 반환합니다.
- YAML 로 저장할 때 키 순서를 유지합니다.

#### 사용 예시

```python
from echoss_common import dict_load, dict_dump

# YAML 파일 읽기
config = dict_load('config/settings.yaml')
print(config)

# JSON 파일 읽기
data = dict_load('config/params.json')

# Properties 파일 읽기
props = dict_load('config/app.properties')

# dict를 YAML 파일로 저장
settings = {'host': 'localhost', 'port': 8080, 'debug': True}
dict_dump(settings, 'config/settings.yaml')

# dict를 JSON 파일로 저장 (들여쓰기 지정 가능)
dict_dump(settings, 'config/settings.json', indent=2)

# XML 파일 읽기와 쓰기
xml_config = dict_load('config/settings.xml')
dict_dump({'settings': {'@version': '1', 'port': '8080'}}, 'config/settings.xml')

# 파일이 이미 존재할 때 덮어쓰기 방지
dict_dump(settings, 'config/settings.yaml', force_write=False)
```

---

## API 참조

### `get_logger(logger_name, logger_format, file_path, backup_count, use_console, level)`

| 파라미터 | 타입 | 기본값 | 설명 |
|----------|------|--------|------|
| `logger_name` | str | `'echoss'` | 로거 이름 |
| `logger_format` | str | `LOG_FORMAT` | 로그 포맷 문자열 |
| `file_path` | str\|None | `None` | 로그 파일 경로 (`None`이면 파일 미사용) |
| `backup_count` | int | `0` | 보관할 로테이팅 파일 수 (0이면 전부 보관) |
| `use_console` | bool | `True` | 콘솔 출력 여부 |
| `level` | str\|int | `'DEBUG'` | 로그 레벨 |

### `get_app_logger(logger_name, logger_format, file_path, backup_count, use_console, level)`

`get_logger()`와 동일한 파라미터를 받지만, `@cache` 데코레이터로 감싸져 있어 동일한 인자로 호출할 경우 항상 같은 로거 인스턴스를 반환합니다. 애플리케이션 전역에서 하나의 로거를 공유할 때 적합합니다.

| 파라미터 | 타입 | 기본값 | 설명 |
|----------|------|--------|------|
| `logger_name` | str | `'echoss'` | 로거 이름 |
| `logger_format` | str | `LOG_FORMAT` | 로그 포맷 문자열 |
| `file_path` | str\|None | `None` | 로그 파일 경로 (`None`이면 파일 미사용) |
| `backup_count` | int | `0` | 보관할 로테이팅 파일 수 |
| `use_console` | bool | `True` | 콘솔 출력 여부 |
| `level` | str\|int | `'DEBUG'` | 로그 레벨 |

### `JsonLogDict(data)`

| 파라미터 | 타입 | 설명 |
|----------|------|------|
| `data` | Any | JSON으로 변환할 값 (dict, list, tuple, str, int, float, bool, None, Mapping 등) |

`str()` 또는 `repr()` 호출 시 `json.dumps()`를 사용해 JSON 문자열을 반환합니다.
`Mapping` 타입은 자동으로 `dict`로 변환되며, JSON 직렬화가 불가능한 객체는 `str()`로 폴백합니다.

### `dict_load(file_path, file_format, **kwargs)`

| 파라미터 | 타입 | 기본값 | 설명 |
|----------|------|--------|------|
| `file_path` | str | — | 읽을 파일 경로 |
| `file_format` | str | `None` | 포맷 강제 지정 (미지정 시 확장자 자동 인식) |

반환은 `dict` 입니다(JSON 최상위가 배열이면 `list`). 파일이 없으면 `FileNotFoundError` 를 냅니다.

### `dict_dump(config, file_path, file_format, force_write, xml_tag, **kwargs)`

| 파라미터 | 타입 | 기본값 | 설명 |
|----------|------|--------|------|
| `config` | dict | — | 저장할 딕셔너리 |
| `file_path` | str | — | 저장할 파일 경로 |
| `file_format` | str | `None` | 포맷 강제 지정 (미지정 시 확장자 자동 인식) |
| `force_write` | bool | `True` | 기존 파일 덮어쓰기 여부 |
| `xml_tag` | str | `None` | XML 루트 태그 (미지정 시 단일 최상위 키 사용) |

---

## 라이선스

Apache License 2.0 (자세한 내용은 [LICENSE](LICENSE) 참고)

echoss AI Bigdata Center

> 참고(비공식 요청, 라이선스 조건 아님): 코드를 수정하여 재배포하실 경우 12cm에 사전 통보해 주시면 감사하겠습니다.
