Metadata-Version: 2.4
Name: kpublic
Version: 0.1.1
Summary: 한국 공공데이터포털(data.go.kr) API를 함정 없이 쓰는 파이썬 SDK — 키 정규화·결과코드 예외·XML 폴백·자동 페이징 + 기상청/에어코리아/공휴일 어댑터
Keywords: korea,public-data,data.go.kr,공공데이터,기상청,에어코리아,공휴일,open-api
Author: Cheolyoung Jang
Author-email: Cheolyoung Jang <ikoweas@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: Korean
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: httpx>=0.27
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/HyaC1107/kpublic
Project-URL: Repository, https://github.com/HyaC1107/kpublic
Project-URL: Changelog, https://github.com/HyaC1107/kpublic/blob/main/CHANGELOG.md
Description-Content-Type: text/markdown

# kpublic

**한국 공공데이터포털(data.go.kr) API를 함정 없이 쓰는 파이썬 SDK.**

포털에서 키를 받고 처음 API를 붙이면 거의 모두가 같은 곳에서 넘어진다.
인코딩 키를 넣어서 `SERVICE_KEY_IS_NOT_REGISTERED_ERROR`, JSON을 요청했는데 XML 에러가 와서 `json.loads` 크래시,
HTTP 200인데 본문 `resultCode`는 실패, 데이터 없음이 `items: ""`로 오는 것, 기상청 격자 좌표…
`kpublic`은 그 함정들을 코어에서 전부 처리하고, 자주 쓰는 API는 어댑터로 감싼다.

```bash
pip install kpublic
```

```python
import kpublic

c = kpublic.Client("포털에서 복사한 키")   # 인코딩 키든 디코딩 키든 그대로 붙여넣으면 된다

# 어댑터 — 서울시청 지금 날씨
kma = kpublic.kma.Forecast(c)
now = kma.ultra_now(lat=37.5665, lon=126.9780)   # 격자 변환·발표시각 계산 자동
now.values            # {'T1H': 23.1, 'RN1': 0.0, 'REH': 78.0, 'PTY': 0.0, ...}
now.label("T1H")      # '기온(℃)'

# 어댑터 — 미세먼지 / 공휴일
kpublic.airkorea.AirKorea(c).by_sido("서울")[0].pm25      # 18.0  (결측 '-'는 None)
kpublic.holidays.Holidays(c).get(2026, 9)                 # [Holiday(date=2026-09-24, name='추석', ...), ...]

# 범용 — 어떤 data.go.kr API든 경로만 알면 된다
r = c.get("1360000/AsosDalyInfoService/getWthrDataList",
          dataCd="ASOS", dateCd="DAY", startDt="20260901", endDt="20260907", stnIds=108, dataType="JSON")
r.items, r.total_count

# 자동 페이징 — totalCount 보고 끝까지
for row in c.iter_items("B552584/ArpltnInfrqncyInqireSvc/getCtprvnRltmMesureDnsty",
                        sidoName="전국", returnType="json", ver="1.3", page_size=100):
    ...
```

## 코어가 대신 처리하는 것

| 함정 | kpublic |
|---|---|
| 인코딩 키(`%2B…`)를 넣으면 두 번 인코딩돼 30번 에러 | 어느 형태든 원본 키로 정규화 → 전송선엔 정확히 한 번 인코딩 |
| 실패해도 HTTP 200, 사유는 본문 `resultCode` | 코드별 예외: `ServiceKeyError`(21/30/31) `RateLimitError`(22) `UnregisteredIPError`(32) `InvalidParameterError`(10/11) `AccessDeniedError`(12/20/33) `ServerError`(01/02/04/05/99) |
| JSON 요청해도 게이트웨이 에러는 XML | 첫 글자 보고 자동 판별, `OpenAPI_ServiceResponse/cmmMsgHeader` 봉투 해석 |
| `items`가 `""` / `null` / 누락 / dict 하나 / list | 전부 `list[dict]` |
| 데이터 없음(03)이 예외인가 | 기본은 빈 `Response`(총건수 0). `raise_on_nodata=True`면 `NoDataError` |
| 페이징 수동 | `iter_items()` — `totalCount`가 없어도 짧은 페이지에서 멈춤 |
| 서버 일시 장애 | 5xx·타임아웃·01/02/04/05/99만 지수 백오프 재시도. **22(일일 초과)·키 오류는 재시도하지 않는다** — 해봐야 소용없고 트래픽만 태운다 |

## 어댑터

| 모듈 | API | 제공 |
|---|---|---|
| `kpublic.kma` | 기상청 단기예보 2.0 | `Forecast.ultra_now / ultra_forecast / village`, `latlon_to_grid`, `grid_to_latlon`, `latest_base_time`(발표시각 규칙) |
| `kpublic.airkorea` | 에어코리아 대기오염정보 | `AirKorea.by_sido / by_station` → `AirQuality`(결측 `None`, 등급 라벨) |
| `kpublic.holidays` | 천문연 특일정보 | `Holidays.get(year, month=None)`, `is_holiday(date)` |

어댑터가 없는 API는 `Client.get(path, **params)`로 그대로 부른다. 코어의 함정 처리는 똑같이 적용된다.

## 활용신청은 API마다 따로

키는 계정당 하나지만 **API마다 활용신청**을 해야 한다(대부분 자동승인). 안 하면 `ServiceKeyError(30)`.

- 기상청 단기예보 — https://www.data.go.kr/data/15084084/openapi.do
- 에어코리아 대기오염 — https://www.data.go.kr/data/15073861/openapi.do
- 천문연 특일정보 — https://www.data.go.kr/data/15012690/openapi.do

신청 후 확인: `KPUBLIC_SERVICE_KEY=발급키 python scripts/smoke.py`

## 검증 상태 (0.1.0, 2026-09-09)

| 항목 | 방법 | 결과 |
|---|---|---|
| 코어 전 경로 · 어댑터 파싱 · 격자 기준값 | 단위 테스트 82개, `httpx.MockTransport` | ✅ |
| 기상청 `ultra_now` (서울시청) | **실서버** | ✅ `T1H 24.0 · REH 59 · WSD 5.0 …` 8개 카테고리 |
| 에어코리아 `by_sido("서울")` | **실서버** | ✅ 측정소 40곳, `"-"` 결측 → `None` 확인 |
| 천문연 `Holidays.get(2026)` | **실서버** (두 번째 키) | ✅ 공휴일 22일 |
| 인코딩 키(`%2B…`) 그대로 넣기 | **실서버** (두 번째 키는 인코딩 형태로 입력) | ✅ 정규화 후 정상 인증 |
| 게이트웨이 오류가 HTTP 400/403 + JSON 봉투로 오는 경우 | **실서버**에서 발견 → 테스트 5개 추가 | ✅ `ServiceKeyError(30)` / `AccessDeniedError(12)` 매핑 |
| 미신청 API 호출 | **실서버** (각 키가 미신청인 API로) | ✅ `ServiceKeyError(30)` + 활용신청 링크 안내 |

키 두 개(각각 다른 API에 신청됨)로 나눠 찍어 어댑터 3종 모두 happy path를 실서버에서 확인했다.

실서버 검증에서 잡힌 것 2건: 게이트웨이 오류의 4xx 처리 순서, 에어코리아 서비스 ID 오타(`ArpltnInfrqncy…` → `ArpltnInforInqireSvc`). 둘 다 단위 테스트만으로는 못 잡는 종류다.

## 개발

```bash
uv sync --group dev
uv run pytest
uv run ruff check . && uv run mypy src
uv build
```

## 배포

토큰 없이 PyPI Trusted Publishing으로 배포한다. `pyproject.toml`·`__init__.py` 버전을 올리고 CHANGELOG에 항목을 추가한 뒤:

```bash
git tag v0.2.0 && git push origin v0.2.0
```

`release.yml`이 태그와 버전이 일치하는지 확인하고 테스트 → 빌드 → 업로드까지 한다.

## 로드맵

- 0.2 — `AsyncClient`(같은 코어), 응답 캐시(sqlite), 기상청 ASOS·중기예보, 한강홍수통제소
- 0.3 — pandas/polars 변환 헬퍼, CLI

## 라이선스

MIT
