Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions API_COVERAGE.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# API coverage

Snapshot date: 2026-06-11
상태 기준일: 2026-09-19 (`traffic.flow` 검증 갱신)

이 문서는 섞이기 쉬운 세 가지 상태를 분리한다.

- **이 저장소에 문서화됨**: `endpoints.md`에 등재됨.
- **구현됨**: `KrexClient` method로 노출됨.
- **Live 검증됨**: `tests/test_live_ex.py`에서 실제 provider를 호출해 확인함.

0405는 2026-09-19에 인증 요청 총 3회로 조사·검증했다. 마지막 공개
`traffic.flow(num_of_rows=20000)` 호출은 8,370행/69개 노선을 모두 파싱했다.
기점 방향 4,188행, 종점 방향 4,182행, 음수 속도 결측 1,520행이었다.
개수는 해당 시점의 관측값이며 고정 계약이 아니다. 인증키나 인증 URL은 저장하지 않았다.
이후 추가한 `flow_all()`과 공개 `vds_id`는 같은 실제 응답 표본 및 가짜 세션으로
검증했다. 추가 실호출은 하지 않았으며 새 메서드의 직접 실호출 검증 상태는 미검증이다.

이 프로젝트는 `data.ex.co.kr`와 `data.go.kr`에 흩어진 한국도로공사 API 전체를 아직 완전히 감싼 wrapper가 아니다. 현재 저장소에 문서화된 endpoint set을 typed client로 제공하고, 더 넓은 공식 coverage는 backlog로 명시한다.

## 현재 저장소 coverage
Expand All @@ -16,7 +23,8 @@ Snapshot date: 2026-06-11
|---|---|---|---:|---:|---|
| `traffic.by_ic()` | `data.ex.co.kr` | `Page[TrafficByIc]` | Yes | Yes | 실제 응답은 top-level `trafficIc`와 `trafficAmout` 같은 field variant를 사용한다. |
| `traffic.by_route()` | `data.ex.co.kr` | `Page[dict]` | Yes | Yes | Empty success가 `count=0`, `list=[]`일 수 있다. |
| `traffic.flow()` | `data.ex.co.kr` | `Page[TrafficFlow]` | Yes | No | 과거 live probe에서 404가 반환되어 portal UI 확인 전까지 unverified로 둔다. |
| `traffic.flow()` | `data.ex.co.kr` | `Page[TrafficFlow]` | Yes | Yes | 2026-09-19 공식 0405 `odtraffic/trafficAmountByRealtime`와 실제 8,370행 확인. 필터·페이지 분할은 로컬 처리. `S/E`는 기점/종점, 시각은 `stdDate/stdHour`, 음수 속도는 결측. 2행 표본으로 오프라인 재현. |
| `traffic.flow_all()` | `data.ex.co.kr` | `Page[TrafficFlow]` | Yes | No | 동일 엔드포인트의 전체 응답을 한 번 조회하고 모든 VDS 행을 반환한다. `flow()`가 재사용한다. 실제 표본의 `vdsId` 및 1,000행 초과·같은 콘존의 여러 VDS 보존을 오프라인 검증. |
| `traffic.incident()` | `data.ex.co.kr` | `Page[Incident]` | Yes | Yes | 구 `trafficapi/incident`는 404로 제거 확인. `burstInfo/realTimeSms`(apiId 0611)로 repoint, 2026-06-11 live 검증 (count=190). 경도는 `altitude` 키로 온다. `realTimeSMSList`와 0 이상인 `count`가 없는 HTTP 200 본문은 파싱 오류로 거부한다. |
| `traffic.vds_raw()` | `data.ex.co.kr` | `Page[dict]` | Yes | No | High-volume raw endpoint이므로 live test는 date/time range를 좁힌다. |
| `traffic.avc_raw()` | `data.ex.co.kr` | `Page[dict]` | Yes | No | `vds_id`, `std_date`가 필요하다. |
Expand Down Expand Up @@ -45,12 +53,12 @@ Snapshot date: 2026-06-11

| Category | Count |
|---|---:|
| `endpoints.md`에 문서화된 method | 26 |
| `KrexClient` namespace에 구현된 method | 27 |
| Typed public model을 반환하는 method | 10 |
| `endpoints.md`에 문서화된 method | 27 |
| `KrexClient` namespace에 구현된 method | 28 |
| Typed public model을 반환하는 method | 11 |
| Raw `dict` record를 반환하는 method | 13 |
| Local reference helper | 3 |
| Provider live 검증 완료 method | 6 |
| Provider live 검증 완료 method | 7 |

## 더 넓은 공식 API backlog

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# 변경 기록

## 2026-09-19 — 실시간 소통 조회 복구

- 인자 없는 `traffic.flow_all()`이 한 응답의 모든 행을 `Page[TrafficFlow]`로 반환한다.
`flow()`는 이를 재사용한 뒤 기존 로컬 필터·페이지 분할을 적용한다.
`TrafficFlow.vds_id: str | None = None`에 공식 `vdsId`를 보존한다.
실제 표본으로 필드명을 검증했고, 같은 콘존의 여러 VDS를 합치지 않는다.
- `traffic.flow()`를 공식 0405 `/openapi/odtraffic/trafficAmountByRealtime`로 수정했다.
서버가 제공하지 않는 필터·페이지 인자는 로컬 처리하며 기존 이름·기본값을 유지한다.
- `stdDate/stdHour`를 기존 `updated_at` 문자열로 조합하고 음수 속도는 결측 처리한다.
`grade=3`은 `CongestionLevel.STOP`으로 매핑하며 `timeAvg`를 자유속도로 사용하지 않는다.
- `FlowDirection.START/END`를 공개했다. 현행 `S/E`는 기점/종점이므로
`flow(direction=...)`에서 기존 `Direction`과 `0/1`을 거부한다.
`TrafficFlow.direction`은 현행 `FlowDirection`과 과거 응답의 `Direction`을 지원한다.
- 목록·건수 및 모든 행을 검증하고 잘못된 응답을 빈 성공이나 일부 성공으로 숨기지 않는다.
실제 응답에서 추출한 인증정보 없는 2행 표본과 회귀 테스트를 추가했다.

## 2026-09-14 — native async와 공통 TPS

KrexClient와 서비스·debug를 native async로 전환하고 동기 bridge/Async 접두사/aio를 제거했다. 공통 AsyncTokenBucket과 max_rps/rate_limiter를 공개한다. 기존 기본 5 TPS·capacity=1과 재시도 과금을 보존하고 리다이렉트·동시 디버그·주입 세션 수명을 함께 처리한다.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@
## 핵심 특징

- **네임스페이스형 클라이언트**: `client.traffic.flow()`, `client.tollfee.between_tollgates()`처럼 문서의 API 범주와 같은 구조로 호출합니다.

`traffic.flow()`는 공식 0405 실시간 소통 데이터를 한 번 조회한 뒤 노선·콘존·방향
필터와 페이지 분할을 로컬에서 적용합니다. 기본 페이지는 최대 1,000행이며
`total_count`는 필터 후 전체 건수입니다. 전체 수집에는
`page = await client.traffic.flow_all()`을 사용하세요. 인자 없이 한 응답의 모든 행을
`Page[TrafficFlow]`로 반환하며 페이지 크기를 추측할 필요가 없습니다.
`flow()`도 내부에서 `flow_all()`을 호출한 뒤 로컬 필터·페이지 처리를 적용합니다.
방향은 `FlowDirection.START`(`S`, 기점)/`END`(`E`, 종점)를 사용합니다.
기존 `Direction.UP/DOWN/EAST/SOUTH`와는 의미가 달라 `flow()` 필터에 사용할 수 없습니다.
`updated_at`은 KST로 취급하는 `YYYYMMDDHHMM` 문자열이며, 음수 속도는 `None`입니다.
같은 콘존의 여러 VDS 행은 그대로 유지하며 공식 `vdsId`는 `item.vds_id`로 제공합니다.
과거 응답에 이 필드가 없으면 `None`입니다. [상세 계약](endpoints.md#실시간-소통)을 참고하세요.
- **두 포털 동시 지원**: `data.ex.co.kr` 키(`KEX_EX_API_KEY`)와 `data.go.kr` 키(`DATA_GO_KR_SERVICE_KEY`)를 분리해 사용합니다.
- **로컬 `.env` 기본 로딩**: 환경변수가 없으면 현재 작업 디렉터리부터 부모 디렉터리의 `.env`를 찾아 키를 읽고, 복붙 과정에서 섞인 공백 문자를 제거합니다.
- **API 카탈로그**: 구현된 API의 함수명, 데이터셋명, 포털, 엔드포인트, 서비스키 발급/활용신청 링크를 `api_catalog()` 또는 `get_api_catalog()`로 조회할 수 있습니다.
Expand Down Expand Up @@ -177,6 +189,7 @@ client = KrexClient(
| 교통 | `traffic.by_ic()` | `data.ex.co.kr` | `Page[TrafficByIc]` |
| 교통 | `traffic.by_route()` | `data.ex.co.kr` | `Page[dict]` |
| 교통 | `traffic.flow()` | `data.ex.co.kr` | `Page[TrafficFlow]` |
| 교통 | `traffic.flow_all()` | `data.ex.co.kr` | `Page[TrafficFlow]` |
| 교통 | `traffic.incident()` | `data.ex.co.kr` | `Page[Incident]` |
| 교통 | `traffic.vds_raw()`, `traffic.avc_raw()` | `data.ex.co.kr` | `Page[dict]` |
| 통행료 | `tollfee.between_tollgates()` | `data.ex.co.kr` | `Page[TollFee]` |
Expand Down
13 changes: 13 additions & 0 deletions codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [진출입 (IOType)](#진출입-iotype)
- [시간 단위 (TimeUnit)](#시간-단위-timeunit)
- [방향 (Direction)](#방향-direction)
- [실시간 소통 방향 (FlowDirection)](#실시간-소통-방향-flowdirection)
- [혼잡도 (CongestionLevel)](#혼잡도-congestionlevel)
- [할인 유형 (DiscountType)](#할인-유형-discounttype)
- [노선 코드](#노선-코드)
Expand Down Expand Up @@ -122,8 +123,20 @@ CarType.from_label("1종") # CarType.LIGHT

---

## 실시간 소통 방향 (FlowDirection)

0405 `traffic.flow()`는 별도 `FlowDirection`을 사용한다.
`FlowDirection.START="S"`는 기점 방향, `FlowDirection.END="E"`는 종점 방향이다.
이 코드들을 기존 `Direction.SOUTH/EAST`나 `UP/DOWN`으로 해석하면 안 된다.
과거 응답의 `dirType`은 기존 `Direction`으로 파싱하는 호환성을 유지한다.

## 혼잡도 (CongestionLevel)

0405의 원본 `grade`는 `0`=판정불가, `1`=원활(80km/h 이상),
`2`=서행(40 이상 80 미만), `3`=정체(0 이상 40 미만)다.
각각 `None`, `SMOOTH`, `SLOW`, `STOP`으로 정규화한다. 아래 공통 enum의
기존 값은 보존하므로 `STOP.value="4"`와 원본 `grade="3"`을 혼동하지 않는다.

실시간 소통 정보의 혼잡 수준.

| 코드 | Enum | 색상 | 통행속도 기준 (대략) |
Expand Down
69 changes: 53 additions & 16 deletions endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,34 +158,71 @@ asyncio.run(main())

### 실시간 소통

도로별 통행속도, 혼잡도(원활/지체/정체) 정보.
VDS별 통행속도와 콘존 정보(apiId `0405`). 같은 콘존에 여러 VDS 관측 행이 있으며
라이브러리는 이들을 집계하거나 중복 제거하지 않는다.

- **포털**: `data.ex.co.kr`
- **경로**: `/openapi/trafficapi/realFlow`
- **공공데이터포털 대응**: 데이터셋 ID `15076684`
- **메서드**: `client.traffic.flow()`
- **경로**: `/openapi/odtraffic/trafficAmountByRealtime`
- **공식 명세**: [실시간 소통 데이터](https://data.ex.co.kr/openapi/basicinfo/openApiInfoM?apiId=0405)
- **메서드**: `client.traffic.flow()`, `client.traffic.flow_all()`

전체 수집은 인자 없는 `page = await client.traffic.flow_all()`을 사용한다.
한 번의 논리적 조회에서 받은 전체 응답을 검증하고 모든 행을 `Page[TrafficFlow]`로
반환한다. 필터·페이지 분할·건수 상한·중복 제거는 없으며 `len(page.items)`와
`page.total_count`는 일치한다. `page_no`와 `num_of_rows`는 `None`, `raw`는
원본 전체 응답이다. 공통 HTTP 재시도 정책은 유지한다.
이는 여러 응답을 페이지별로 합치지 않는다는 보장이며, 공급자가 모든 VDS의
수집시각을 같게 보장한다는 뜻은 아니다.

**파라미터**

| 이름 | 필수 | 타입 | 설명 |
|------|------|------|------|
| `routeNo` | O | `code` | 노선번호 (생략 시 전체) |
| `conzoneId` | O | `str` | 콘존 ID (구간 ID) |
| `dirType` | O | `code` | 방향 |
| `key` | 필수 | `str` | 인증키, 클라이언트가 자동 제공 |
| `type` | 필수 | `str` | `json`, 클라이언트가 자동 제공 |

2026-09-19 공식 동적 메타데이터(`openApiInfoDetail`, `openApiInfoPop`)에는
필터·페이지 입력이 없다. 실제 `routeNo=0010`, `numOfRows=2`, `pageNo=2` 요청도
69개 노선 8,370행 전체를 반환했다. 따라서 이 인자들을 서버로 보내지 않는다.

기존 Python 인자 중 `route_no`, `conzone_id`, `num_of_rows=1000`, `page_no=1`을
유지하며 `flow_all()`로 전체 응답 검증 → 로컬 필터 → 로컬 페이지 분할 순으로 적용한다.
`direction`은 `FlowDirection.START`/`END` 또는 `"S"`/`"E"`만 받는다.
의미가 다른 `Direction` 인스턴스와 `"0"`/`"1"`은 요청 전에 거부한다.
`Page.total_count`는 필터 후 건수, `Page.raw["count"]`는 서버 전체 건수다.
페이지 크기·번호는 양의 정수여야 한다. 필터 결과가 없거나 마지막 페이지를 넘으면 빈 페이지다.
각 호출은 전체 응답을 다시 가져오므로 다른 페이지 호출 사이에 수집 시각이 바뀔 수 있다.
전체 자료가 필요하면 `flow_all()`을 한 번 호출한다. 자동 반복 호출은 하지 않는다.

**응답 필드**

| 필드 | 설명 |
|------|------|
| `conzoneId` | 구간 식별자 |
| `conzoneName` | 구간명 (예: "서울→수원") |
| `routeNo` / `routeName` | 노선 번호/명 |
| `speed` | 평균 통행속도 (km/h) |
| `tmFreeFlow` | 자유속도 |
| `congestionLevel` | `1`=원활, `2`=서행, `3`=지체, `4`=정체 |
| `updTime` | 갱신 시각 |

> **갱신 주기**: 5분. 더 빠른 폴링은 같은 데이터를 반환하므로 호출 한도만 소모.
| `conzoneId` / `conzoneName` | 구간 식별자/명 → `conzone_id` / `conzone_name` |
| `routeNo` / `routeName` | 노선 번호/명. 선행 0 보존 |
| `vdsId` | VDS 식별자 → `vds_id: str \| None = None`. 선행 0과 원문 `raw` 보존. 실제 표본은 `0010VDE00100`, `0010VDS00100`. 과거 응답의 필드 누락은 `None` |
| `stdDate` / `stdHour` | 수집일자 `YYYYMMDD` / 수집시각 `HHMM` → `updated_at=YYYYMMDDHHMM` 문자열. 한국 현지 시각(KST)으로 취급 |
| `updownTypeCode` | `S`=기점 방향 → `FlowDirection.START`, `E`=종점 방향 → `FlowDirection.END`. 동서남북 또는 상하행으로 추정하지 않음 |
| `speed` | 속도(km/h) → `speed`. 실제 음수 결측값은 `None`, 0은 정지 속도로 보존 |
| `grade` | `0`=판정불가 → `None`, `1`=원활(80 이상) → `SMOOTH`, `2`=서행(40 이상 80 미만) → `SLOW`, `3`=정체(0 이상 40 미만) → `STOP` |
| `timeAvg` | 통행시간, `raw`에 보존. 공식 메타데이터에 단위 없음. 자유속도로 사용하지 않음 |
| `trafficAmout` / `shareRatio` | 교통량(대) / 점유율, `raw`에 보존 |

`TrafficFlow`의 기존 필드명과 `updTime`, `avgSpeed`, `dirType` 등 과거 응답 별칭은
유지한다. 현행 응답은 자유속도를 주지 않으므로 `free_flow_speed=None`이다.
기존 공통 `CongestionLevel.STOP`의 값은 `"4"`이며, 0405 원본 `grade="3"`과 다르다.
원본 등급·방향·음수 속도는 항상 `raw`에서 확인할 수 있다.

두 메서드 모두 응답에 `list`와 정확한 전체 `count`가 있어야 한다. 누락·잘못된 타입·건수 불일치·
일부 행 파싱 실패는 `KrexParseError`다. 명시적인 `count=0, list=[]`는 정상 빈 응답이다.
`NO_DATA` 오류는 기존 `strict_no_data` 설정을 따르지만, 잘못된 응답은 이 설정과
무관하게 거부한다. 검증을 통과한 단일 객체 `list`도 1행으로 정규화한다.

근거는 공식 [JavaScript](https://data.ex.co.kr/js/sub/openApiInfo.js)가 호출하는
`POST /openapi/basicinfo/openApiInfo`, `openApiInfoDetail`, `openApiInfoPop`
(`apiId=0405`)와 실제 응답이다. 명세에는 시간대·갱신 주기·통행시간 단위가 명시되지
않았다. KST 해석은 한국 현지 수집시각과 검증 시각이 일치한 관측에 근거하며,
갱신 주기를 5분으로 단정하지 않는다.

---

Expand Down
3 changes: 3 additions & 0 deletions error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ asyncio.run(main())

기본 `strict_no_data=True`는 공급자의 NO_DATA를 예외로 전달한다.
빈 Page를 받으려면 생성자에 `strict_no_data=False`를 명시한다.
`traffic.flow()`와 `traffic.flow_all()`의 명시적인 `count=0, list=[]`는 정상 빈 응답이다.
목록·건수 누락, 건수 불일치, 일부 행 파싱 실패는 `strict_no_data=False`에서도
`KrexParseError`로 거부한다. 빈 필터 결과나 범위 밖 로컬 페이지는 빈 `Page`다.
`latest_weather`는 과거 시간대를 찾는 목적에 맞게 NO_DATA인 시간대를 건너뛴다.

```python
Expand Down
2 changes: 2 additions & 0 deletions src/krex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CoordinateSystem,
Direction,
DiscountType,
FlowDirection,
IOType,
KrexCode,
RoadOperator,
Expand Down Expand Up @@ -64,6 +65,7 @@
"DiscountType",
"DebugRun",
"FoodPrice",
"FlowDirection",
"IOType",
"Incident",
"KrexCode",
Expand Down
14 changes: 12 additions & 2 deletions src/krex/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,20 @@ def _go(
_ex(
"traffic.flow",
"한국도로공사_실시간 소통정보",
"/openapi/trafficapi/realFlow",
"/openapi/odtraffic/trafficAmountByRealtime",
"Page[TrafficFlow]",
"콘존별 실시간 속도와 정체 상태입니다.",
"VDS별 실시간 속도와 콘존 정보입니다. 필터와 페이지 분할은 로컬에서 처리합니다.",
live_verified=True,
fixture_supported=True,
),
_ex(
"traffic.flow_all",
"한국도로공사_실시간 소통정보 전체",
"/openapi/odtraffic/trafficAmountByRealtime",
"Page[TrafficFlow]",
"하나의 전체 응답에 포함된 모든 VDS 관측값을 필터·페이지 분할 없이 반환합니다.",
live_verified=False,
fixture_supported=True,
),
_ex(
"traffic.incident",
Expand Down
Loading
Loading