diff --git a/AGENTS.md b/AGENTS.md index 8377017..9c3f147 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # AGENTS.md +네트워크 I/O는 비동기 전용이다(ADR-006). `await`/`async for`/`async with`를 사용하며, Async 접두사 별칭·aio 팩터리·동기 네트워크 경로를 추가하지 않는다. JSON/XML `resultCode=03`은 최신 계약에 따라 빈 결과로 정규화한다. + ## 문서 언어 정책 이 저장소의 모든 Markdown/RST 문서는 한글로 작성합니다. 공식 API 필드명, 코드 식별자, 명령어, URL, provider 원문처럼 그대로 보존해야 하는 값만 영어를 유지합니다. 새 문서나 기존 문서를 수정할 때도 이 규칙을 우선합니다. @@ -57,7 +59,7 @@ PC 개발은 Windows 호스트에서 직접 진행합니다. 본 저장소는 Py 3. **기본 테스트에서 실제 API 호출 금지** — 네트워크 호출 없는 mock/fixture 기반으로 검증해야 합니다. 실제 호출 테스트를 추가할 경우 `DATA_GO_KR_SERVICE_KEY`가 있을 때만 실행되도록 `integration` marker를 사용합니다. 4. **`nx`/`ny`를 위도/경도로 취급 금지** — WGS84 좌표는 항상 `lat/lon` 순서로 다루며, KMA 격자 좌표(`nx/ny`)와 엄격히 구분합니다. 외부 프로그램용 위치 입력은 `LatLon`/`GridPoint` 또는 `location=`으로 표준화합니다. 5. **`PCP`, `SNO` 범주 문자열을 무조건 float로 변환 금지** — `"1.0mm 미만"`, `"30.0~50.0mm"`, `"강수없음"` 같은 범주 문자열은 무리하게 숫자로 바꾸지 않고 보존합니다. 대표값이 필요할 때만 `parse_amount()`를 제공합니다. -6. **KMA result code 실패를 빈 리스트 성공처럼 반환 금지** — `resultCode != "00"`은 반드시 명시적인 typed exception으로 surface합니다. +6. **KMA result code 실패를 빈 리스트 성공처럼 반환 금지** — `resultCode`가 "00"/"03" 이외인 경우는 반드시 명시적인 typed exception으로 surface합니다. 7. **data.go.kr와 APIHub의 인증 파라미터 혼용 금지** — data.go.kr 키는 `DATA_GO_KR_SERVICE_KEY`, APIHub 키는 `KMA_APIHUB_AUTH_KEY`로 엄격히 분리하여 사용합니다. 8. **APIHub endpoint가 항상 JSON을 반환한다고 가정 금지** — 텍스트, 이미지, 바이너리 응답이 섞여 있으므로 `response_kind`나 `content` 타입을 명확히 처리합니다. 9. **불필요한 wrapper/adapter 계층 추가 금지** — 단순 전달용 wrapper, 장기 호환 alias, 임시 facade를 지양하고, 다른 라이브러리에 검증된 구현이 있으면 라이선스와 출처를 확인한 뒤 프로젝트 내부 구현으로 직접 반영합니다. @@ -122,7 +124,7 @@ PC 개발은 Windows 호스트에서 직접 진행합니다. 본 저장소는 Py - `dataType=JSON`을 기본으로 둡니다. - `pageNo`, `numOfRows` 기본값이 있습니다. - fake session 테스트가 요청 파라미터를 검증합니다. -- `resultCode != "00"`은 typed exception입니다. +- `resultCode`가 "00"/"03" 이외인 경우는 typed exception입니다. ### data.go.kr 범용 클라이언트 diff --git a/AI_AGENT_GUIDE.md b/AI_AGENT_GUIDE.md index b188fb7..b3ff8ed 100644 --- a/AI_AGENT_GUIDE.md +++ b/AI_AGENT_GUIDE.md @@ -1,5 +1,7 @@ # AI 에이전트 가이드: python-kma-api (kma) +네트워크 I/O는 비동기 전용이다(ADR-006). `await`/`async for`/`async with`를 사용하며, Async 접두사 별칭·aio 팩터리·동기 네트워크 경로를 추가하지 않는다. JSON/XML `resultCode=03`은 최신 계약에 따라 빈 결과로 정규화한다. + 이 라이브러리(`kma`)를 임포트하여 사용하는 외부 기상 정보 서비스 및 소비자 앱(예: TripMate, tour-map, kraddr 등)의 코드를 생성하는 AI 코딩 어시스턴트(Cursor, Copilot, ChatGPT, Claude Code 등)를 위한 컨텍스트 문서입니다. > **본 저장소(`python-kma-api`) 자체를 수정하려는 에이전트는 다른 문서를 봅니다**: @@ -26,30 +28,43 @@ ### 2.1. `KmaClient` (단기예보 3종 및 버전) -동기 호출은 `KmaClient.from_env()`, 비동기 호출은 `KmaClient.aio_from_env()` 패턴을 활용합니다. +`KmaClient.from_env()`로 생성하고 `async with`/`await`로 호출합니다. ```python +import asyncio from kma import KmaClient, LatLon, GridPoint -# 동기 호출 예시 -with KmaClient.from_env() as kma: - # 1. 초단기실황 (현재 날씨 스냅샷) - snap = kma.forecast.now(lat=37.5665, lon=126.9780) # 서울시청 - print(f"기온: {snap.temperature}°C, 하늘상태: {snap.sky_label}, 강수: {snap.precipitation_label}") + +async def main() -> None: + # 조회 예시 + async with KmaClient.from_env() as kma: + # 1. 초단기실황 (현재 날씨 스냅샷) + snap = (await kma.forecast.now(lat=37.5665, lon=126.9780)) # 서울시청 + print(f"기온: {snap.temperature}°C, 하늘상태: {snap.sky_label}, 강수: {snap.precipitation_label}") - # 2. 단기예보 (향후 3일 일기예보 목록) - items = kma.forecast.vilage(location=LatLon(37.5665, 126.9780)) - for item in items[:5]: - print(item.forecast_at, item.category, item.value, item.label) + # 2. 단기예보 (향후 3일 일기예보 목록) + items = (await kma.forecast.vilage(location=LatLon(37.5665, 126.9780))) + for item in items[:5]: + print(item.forecast_at, item.category, item.value, item.label) + + +asyncio.run(main()) ``` ```python -# 비동기 호출 예시 +import asyncio from kma import KmaClient -async with KmaClient.aio_from_env() as kma: - # 3. 초단기예보 (향후 6시간 대략 예보) - short_items = await kma.forecast.short(nx=60, ny=127) + +async def main() -> None: + # 비동기 호출 예시 + + async with KmaClient.from_env() as kma: + # 3. 초단기예보 (향후 6시간 대략 예보) + short_items = await kma.forecast.short(nx=60, ny=127) + + +asyncio.run(main()) ``` --- @@ -69,7 +84,7 @@ async with KmaClient.aio_from_env() as kma: kma.forecast.now(location=LatLon(37.5665, 126.9780)) kma.forecast.now(location=GridPoint(60, 127)) kma.forecast.now(location={"latitude": 37.5665, "longitude": 126.9780}) - ``` +``` ### 3.2. PCP/SNO 강수량/적설량의 무리한 수치 변환 금지 - 기상청 단기예보 응답에서 강수량(`PCP`), 적설량(`SNO`)은 `"1.0mm 미만"`, `"30.0~50.0mm"`, `"강수없음"` 같은 범주 문자열을 반환합니다. @@ -81,7 +96,7 @@ async with KmaClient.aio_from_env() as kma: parse_amount("1.0mm 미만") # 0.5 (반환됨) parse_amount("30.0~50.0mm") # 40.0 parse_amount("강수없음") # 0.0 - ``` +``` ### 3.3. KST 발표시각의 이해 - 기상청 API는 실시간 기상 데이터를 반환하지 못하고, 특정 발표 주기(정각, 30분, 단기예보 하루 8회)가 있으며 서버 반영 지연시간(10분~40분)이 있습니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e557ab..e1d830f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## 0.1.0 - 미배포 +### 비동기 전용 전환 + +- 네트워크 클라이언트를 단일 async API로 통합하고 Async/aio 별칭을 제거했다. +- 공통 AsyncTokenBucket의 max_rps/rate_limiter로 첫 송신·재시도·redirect TPS를 제어한다. +- 생성기·CLI·디버그·페이지도 같은 계약을 적용하며 기존 모델·파싱·NO_DATA03 동작을 유지한다. + + ### 수정 - asyncio 전환 재검증을 위한 2인 적대적 리뷰어 서브에이전트(동시성/자원관리 관점, 보안/데이터 diff --git a/README.md b/README.md index 36cef25..757a1f1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # python-kma-api +네트워크 API는 비동기 전용이며 기본 5 TPS다. 호출 전환과 공유 버킷 설정은 [비동기 API와 공통 TPS](docs/async-tps.md)를 참고한다. + ![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg) ![GPL-3.0-or-later 라이선스](https://img.shields.io/badge/License-GPL--3.0--or--later-blue.svg) ![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg) @@ -44,7 +46,7 @@ Korea Meteorological Administration(KMA, 기상청) 공공데이터포털과 API ## 핵심 특징 - **공식 단기예보 3종 우선 지원**: `getUltraSrtNcst`, `getUltraSrtFcst`, `getVilageFcst`를 `KmaClient`에서 호출합니다. -- **httpx 기반 sync/async 클라이언트**: 동기 호출은 `KmaClient`, 비동기 호출은 `async with KmaClient.aio(...)` 형태를 사용하며, 예보 API는 `client.forecast.now()`처럼 service facade 아래에 모았습니다. +- **httpx 비동기 전용 클라이언트**: `async with KmaClient(...)` 안에서 `await client.forecast.now()`처럼 호출합니다. - **data.go.kr 범용 호출, 기상청 카탈로그, 주요 helper 지원**: `DataGoKrClient`로 `MidFcstInfoService`, `AsosDalyInfoService`, `WthrWrnInfoService` 같은 KMA REST 서비스를 호출하고, 공공데이터포털 `기상청` 검색 전체 페이지의 KMA 항목 86개와 gateway operation 160개를 카탈로그로 조회합니다. - **APIHub 범용 호출과 함수형 래퍼 지원**: `ApiHubClient`로 임의 path를 호출하고, `ApiHubGeneratedClient`로 공식 목록의 470개 endpoint를 함수 이름으로 호출합니다. - **API 카탈로그와 디버그 UI 보조**: `api_catalog()`로 데이터셋명, gateway, operation, 인증키 링크가 있는 선택 목록을 얻고 Streamlit 디버그 화면에서 확인할 수 있습니다. @@ -69,7 +71,7 @@ Korea Meteorological Administration(KMA, 기상청) 공공데이터포털과 API | 분류 | 권장 API | |---|---| -| typed client | `KmaClient`, `AsyncKmaClient`, `DataGoKrClient`, `ApiHubClient` | +| typed client | `KmaClient`, `DataGoKrClient`, `ApiHubClient` | | API 카탈로그 | `KMA_DATA_GOKR_DATASETS`, `DataGoKrDatasetSpec`, `ApiCatalogEntry`, `api_catalog` | | 인증키 로딩 | `api_key_for_gateway`, `env_names_for_gateway`, `load_local_env` | | 위치 값 객체 | `LatLon`, `GridPoint`, `normalize_location` | @@ -131,25 +133,37 @@ pip install -e ".[dev]" ### 3단계: 사용 ```python +import asyncio from kma import KmaClient -with KmaClient.from_env() as kma: - snap = kma.forecast.now(lat=37.5665, lon=126.9780) # 서울시청 - print(snap.temperature, snap.precipitation_label) - items = kma.forecast.vilage(lat=37.5665, lon=126.9780) - for item in items[:5]: - print(item.forecast_at, item.category, item.value, item.label) +async def main() -> None: + async with KmaClient.from_env() as kma: + snap = (await kma.forecast.now(lat=37.5665, lon=126.9780)) # 서울시청 + print(snap.temperature, snap.precipitation_label) + + items = (await kma.forecast.vilage(lat=37.5665, lon=126.9780)) + for item in items[:5]: + print(item.forecast_at, item.category, item.value, item.label) + + +asyncio.run(main()) ``` -비동기 코드는 `python-krheritage-api`와 같은 facade 패턴을 따릅니다. +비동기 클라이언트는 컨텍스트 종료 시 내부 세션을 닫습니다. ```python +import asyncio from kma import KmaClient -async with KmaClient.aio_from_env() as kma: - snap = await kma.forecast.now(nx=60, ny=127) - items = await kma.forecast.short(nx=60, ny=127) + +async def main() -> None: + async with KmaClient.from_env() as kma: + snap = await kma.forecast.now(nx=60, ny=127) + items = await kma.forecast.short(nx=60, ny=127) + + +asyncio.run(main()) ``` KMA 예보 응답은 시간대가 아니라 category row 단위로 나뉘어 있으므로, 화면/저장 경계에서는 시간축으로 피벗하면 다루기 쉽습니다. @@ -165,24 +179,50 @@ print(first.forecast_at, first.value(WeatherCategory.TEMPERATURE), first.label(" 격자 좌표를 이미 알고 있다면 `nx`/`ny`를 직접 사용할 수 있습니다. ```python -items = kma.forecast.vilage(nx=60, ny=127) +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + items = (await kma.forecast.vilage(nx=60, ny=127)) + + +asyncio.run(main()) ``` 외부 프로그램에서는 위치를 명시적인 값 객체로 넘기는 방식을 권장합니다. ```python +from kma import KmaClient +import asyncio from kma import GridPoint, LatLon -snap = kma.forecast.now(location=LatLon(37.5665, 126.9780)) -items = kma.forecast.vilage(location=GridPoint(60, 127)) -short = kma.forecast.short(location={"latitude": 37.5665, "longitude": 126.9780}) + +async def main() -> None: + async with KmaClient.from_env() as kma: + snap = (await kma.forecast.now(location=LatLon(37.5665, 126.9780))) + items = (await kma.forecast.vilage(location=GridPoint(60, 127))) + short = (await kma.forecast.short(location={"latitude": 37.5665, "longitude": 126.9780})) + + +asyncio.run(main()) ``` dict 기반 입력도 지원합니다. API 서버나 설정 파일에서 받은 값을 그대로 연결할 때 유용합니다. ```python -kma.forecast.now(location={"latitude": 37.5665, "longitude": 126.9780}) -kma.forecast.now(location={"nx": 60, "ny": 127}) +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + (await kma.forecast.now(location={"latitude": 37.5665, "longitude": 126.9780})) + (await kma.forecast.now(location={"nx": 60, "ny": 127})) + + +asyncio.run(main()) ``` 좌표 변환만 사용할 수도 있습니다. 기존 tuple 기반 API는 하위 호환용으로 유지합니다. @@ -235,14 +275,20 @@ APIHub 공식 목록 기반 함수형 래퍼는 470개이며, 포맷정보/예 `data.go.kr`의 다른 KMA 서비스는 `DataGoKrClient`를 사용합니다. ```python +import asyncio from kma import DataGoKrClient -client = DataGoKrClient.from_env() -items = client.items( - "MidFcstInfoService", - "getMidFcst", - {"stnId": "108", "tmFc": "202605010600"}, -) + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + items = (await client.items( + "MidFcstInfoService", + "getMidFcst", + {"stnId": "108", "tmFc": "202605010600"}, + )) + + +asyncio.run(main()) ``` data.go.kr 문서가 인증키 파라미터를 `ServiceKey`로 표기한 서비스는 다음처럼 바꿀 수 있습니다. @@ -254,22 +300,30 @@ client = DataGoKrClient.from_env(service_key_param="ServiceKey") 공공데이터포털 `기상청` 오픈 API 검색 전체 페이지에서 확인한 KMA 항목은 카탈로그로 확인할 수 있습니다. 제목이 `기상청`으로 시작하지 않는 검색 결과는 포함하지 않습니다. 카탈로그에는 KMA 항목 86개, 기존 data.go.kr `serviceKey` gateway operation 160개, APIHub LINK 항목 48개가 들어 있습니다. ```python +from kma import DataGoKrClient +import asyncio from kma import KMA_DATA_GOKR_DATASETS, api_catalog -print(len(KMA_DATA_GOKR_DATASETS)) # 86 -for entry in api_catalog(gateway="datagokr")[:3]: - print(entry.dataset_name, entry.operation, entry.service_key_url) -spec = client.dataset("15059093") -rows = client.dataset_items( - "15059093", - { - "startDt": "20260501", - "endDt": "20260502", - "dataCd": "ASOS", - "dateCd": "DAY", - }, -) +async def main() -> None: + async with DataGoKrClient.from_env() as client: + print(len(KMA_DATA_GOKR_DATASETS)) # 86 + for entry in api_catalog(gateway="datagokr")[:3]: + print(entry.dataset_name, entry.operation, entry.service_key_url) + + spec = client.dataset("15059093") + rows = (await client.dataset_items( + "15059093", + { + "startDt": "20260501", + "endDt": "20260502", + "dataCd": "ASOS", + "dateCd": "DAY", + }, + )) + + +asyncio.run(main()) ``` 여러 operation을 가진 dataset은 `operation=`을 명시합니다. APIHub로 연결된 항목은 `gateway="apihub"`로 표시되며 `ApiHubClient` 또는 `ApiHubGeneratedClient`를 사용합니다. @@ -279,31 +333,49 @@ rows = client.dataset_items( 중기예보는 `DataGoKrClient`의 명시적 helper를 사용할 수 있습니다. `reg_id`는 단기예보의 `nx`/`ny`와 다른 KMA 중기예보 권역 코드이며, `kma`는 임의 매핑을 추측하지 않습니다. `tm_fc`를 생략하면 06:00/18:00 발표와 10분 지연을 반영해 최신 조회 가능 `tmFc`를 고릅니다. ```python -rows = client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600") -latest_rows = client.mid_land_forecast(reg_id="11B00000") -temps = client.mid_temperature_forecast(reg_id="11B10101", tm_fc="202605010600") -overview = client.mid_forecast(stn_id="108", tm_fc="202605010600") -sea = client.mid_sea_forecast(reg_id="12A20000", tm_fc="202605010600") -asos = client.asos_daily_weather(start_dt="20260501", end_dt="20260502", stn_ids=108) -warnings = client.weather_warning_list(stn_id=108, from_tm_fc="20260501", to_tm_fc="20260502") -situation = client.weather_situation(stn_id=108) -uv = client.uv_index(area_no="1100000000", time="2026050106") -quake = client.earthquake_message_list(from_tm_fc="20260501", to_tm_fc="20260502") +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + rows = (await client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600")) + latest_rows = (await client.mid_land_forecast(reg_id="11B00000")) + temps = (await client.mid_temperature_forecast(reg_id="11B10101", tm_fc="202605010600")) + overview = (await client.mid_forecast(stn_id="108", tm_fc="202605010600")) + sea = (await client.mid_sea_forecast(reg_id="12A20000", tm_fc="202605010600")) + asos = (await client.asos_daily_weather(start_dt="20260501", end_dt="20260502", stn_ids=108)) + warnings = (await client.weather_warning_list(stn_id=108, from_tm_fc="20260501", to_tm_fc="20260502")) + situation = (await client.weather_situation(stn_id=108)) + uv = (await client.uv_index(area_no="1100000000", time="2026050106")) + quake = (await client.earthquake_message_list(from_tm_fc="20260501", to_tm_fc="20260502")) + + +asyncio.run(main()) ``` 해수욕장 날씨 조회서비스(`BeachInfoservice`)는 전용 helper가 있습니다. ```python -beach_forecast = client.beach_forecast(beach_num=1) -ultra = client.beach_ultra_short_forecast( - beach_num=1, - base_date="20220622", - base_time="1230", -) -waves = client.beach_wave_height(beach_num=1, search_time="202205011600") -tides = client.beach_tide_info(beach_num=1, base_date="20220620") -sun = client.beach_sun_info(beach_num=1, base_date="20220501") -water = client.beach_water_temperature(beach_num=1, search_time="202205011600") +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + beach_forecast = (await client.beach_forecast(beach_num=1)) + ultra = (await client.beach_ultra_short_forecast( + beach_num=1, + base_date="20220622", + base_time="1230", + )) + waves = (await client.beach_wave_height(beach_num=1, search_time="202205011600")) + tides = (await client.beach_tide_info(beach_num=1, base_date="20220620")) + sun = (await client.beach_sun_info(beach_num=1, base_date="20220501")) + water = (await client.beach_water_temperature(beach_num=1, search_time="202205011600")) + + +asyncio.run(main()) ``` `beach_forecast()`와 `beach_ultra_short_forecast()`는 `base_date`/`base_time`을 생략하면 KST 기준 최신 발표시각을 자동 선택합니다. `beach_sun_info()`는 공공데이터포털 Swagger의 `Base_date` 파라미터 표기를 그대로 사용합니다. @@ -311,31 +383,46 @@ water = client.beach_water_temperature(beach_num=1, search_time="202205011600") 페이지가 있는 data.go.kr 응답은 helper로 순회할 수 있습니다. `max_pages` 또는 `max_items` guard를 항상 둡니다. ```python -for body in client.iter_pages( - "MidFcstInfoService", - "getMidLandFcst", - {"regId": "11B00000", "tmFc": "202605010600"}, - num_of_rows=100, - max_pages=10, -): - ... +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + async for body in (client.iter_pages( + "MidFcstInfoService", + "getMidLandFcst", + {"regId": "11B00000", "tmFc": "202605010600"}, + num_of_rows=100, + max_pages=10, + )): + ... + + +asyncio.run(main()) ``` APIHub는 별도 인증키(`authKey`)를 사용합니다. ```python +import asyncio from kma import ApiHubClient, ApiHubGeneratedClient -hub = ApiHubClient.from_env() # KMA_APIHUB_AUTH_KEY 또는 KMA_APIHUB_KEY -response = hub.request_path( - "/api/typ01/url/wrn_reg.php", - {"tmfc": "0"}, -) -print(response.text) -generated = ApiHubGeneratedClient.from_env() -asos = generated.kma_sfctm2(tm="202605010900", stn="108", help="1") -rows = asos.text_table().rows +async def main() -> None: + async with ApiHubClient.from_env() as hub: + response = (await hub.request_path( + "/api/typ01/url/wrn_reg.php", + {"tmfc": "0"}, + )) + print(response.text) + + async with ApiHubGeneratedClient.from_env() as generated: + asos = (await generated.kma_sfctm2(tm="202605010900", stn="108", help="1")) + rows = asos.text_table().rows + + +asyncio.run(main()) ``` 자세한 내용은 [docs/datagokr.md](docs/datagokr.md)와 [docs/apihub.md](docs/apihub.md)를 참고하세요. @@ -347,22 +434,41 @@ rows = asos.text_table().rows 사용자에게 반환하는 주요 응답은 Pydantic v2 `BaseModel` 기반의 frozen 모델입니다. ```python -snapshot = kma.now(location=LatLon(37.5665, 126.9780)) -payload = snapshot.model_dump(mode="json") -schema = snapshot.model_json_schema() +from kma import LatLon +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + snapshot = (await kma.now(location=LatLon(37.5665, 126.9780))) + payload = snapshot.model_dump(mode="json") + schema = snapshot.model_json_schema() + + +asyncio.run(main()) ``` `raw`는 provider 원문 row/payload를 보존하고, `metadata`는 저장/캐시/감사 추적에 필요한 provenance를 담습니다. `serviceKey`, `authKey`, `key` 원문은 `metadata.request_params`, 예외 metadata, repr에 남기지 않습니다. ```python -snapshot = kma.now(nx=60, ny=127) +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + snapshot = (await kma.now(nx=60, ny=127)) -raw_for_db = snapshot.model_dump(mode="json") -serving_payload = { - "temperature": raw_for_db["temperature"], - "observed_at": raw_for_db["observed_at"], - "source": raw_for_db["metadata"], -} + raw_for_db = snapshot.model_dump(mode="json") + serving_payload = { + "temperature": raw_for_db["temperature"], + "observed_at": raw_for_db["observed_at"], + "source": raw_for_db["metadata"], + } + + +asyncio.run(main()) ``` `ResponseMetadata` 주요 필드: @@ -435,10 +541,18 @@ class ForecastItem(BaseModel): ### `ForecastTimepoint` ```python +from kma import KmaClient +import asyncio from kma import pivot_forecast_items -points = pivot_forecast_items(kma.forecast(nx=60, ny=127)) -print(points[0].forecast_at, points[0].values["TMP"]) + +async def main() -> None: + async with KmaClient.from_env() as kma: + points = pivot_forecast_items((await kma.forecast(nx=60, ny=127))) + print(points[0].forecast_at, points[0].values["TMP"]) + + +asyncio.run(main()) ``` `ForecastTimepoint`는 같은 `forecast_at`, `nx`, `ny`를 가진 `ForecastItem`을 하나로 묶고 category code를 `values`의 key로 둡니다. `labels`, `units`, `raw_items`, `metadata`도 함께 보존하므로 프론트엔드나 BFF 계층에서 row를 다시 조립하지 않아도 됩니다. @@ -446,10 +560,16 @@ print(points[0].forecast_at, points[0].values["TMP"]) ### `MidForecastItem` ```python +import asyncio from kma import DataGoKrClient -client = DataGoKrClient.from_env() -items = client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600") + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + items = (await client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600")) + + +asyncio.run(main()) ``` `MidForecastItem`은 `MidFcstInfoService` row의 `operation`, `tm_fc`, `reg_id`, `stn_id`, `raw`, `metadata`를 담습니다. 중기예보의 `reg_id`는 단기예보 `nx`/`ny`와 다른 식별자이므로, 라이브러리는 좌표나 권역 매핑을 추측하지 않습니다. @@ -601,10 +721,19 @@ KmaError 모든 `KmaError` 하위 예외는 선택적 metadata 속성을 가질 수 있습니다. ```python -try: - kma.now(nx=60, ny=127) -except KmaError as exc: - print(exc.failure_kind, exc.retryable, exc.metadata) +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + try: + (await kma.now(nx=60, ny=127)) + except KmaError as exc: + print(exc.failure_kind, exc.retryable, exc.metadata) + + +asyncio.run(main()) ``` `failure_kind`는 `auth`, `quota`, `rate_limit`, `request`, `server`, `parse`, `network` 중 하나로 채워질 수 있습니다. 기존처럼 `except KmaAuthError`, `except KmaRequestError`로 잡는 코드는 그대로 동작합니다. @@ -614,17 +743,26 @@ except KmaError as exc: ## Pagination과 Cache Key ```python +from kma import latest_mid_fcst_time +from kma import DataGoKrClient +import asyncio from kma import cache_expire_at, has_next_page, make_cache_key, next_page_no -body = client.request("MidFcstInfoService", "getMidLandFcst", {...}) -if has_next_page(body): - print(next_page_no(body)) -key = make_cache_key( - "getVilageFcst", - {"base_date": "20260507", "base_time": "0200", "nx": 60, "ny": 127}, -) -expire_at = cache_expire_at("getVilageFcst", "20260507", "0200") +async def main() -> None: + async with DataGoKrClient.from_env() as client: + body = (await client.request("MidFcstInfoService", "getMidLandFcst", {"regId": "11B00000", "tmFc": latest_mid_fcst_time()})) + if has_next_page(body): + print(next_page_no(body)) + + key = make_cache_key( + "getVilageFcst", + {"base_date": "20260507", "base_time": "0200", "nx": 60, "ny": 127}, + ) + expire_at = cache_expire_at("getVilageFcst", "20260507", "0200") + + +asyncio.run(main()) ``` `make_cache_key()`는 `serviceKey`, `authKey`, `key`를 제거한 sanitized params를 사용합니다. 같은 endpoint, 같은 기준시각, 같은 `nx`/`ny` 조합이면 인증키가 달라도 같은 cache key가 만들어집니다. diff --git a/SKILL.md b/SKILL.md index 0af7ba2..f40d2ae 100644 --- a/SKILL.md +++ b/SKILL.md @@ -5,6 +5,8 @@ description: 기상청 공공 날씨 API용 Python 클라이언트를 구현, # KMA Python 라이브러리 빌더 +네트워크 I/O는 비동기 전용이다(ADR-006). `await`/`async for`/`async with`를 사용하며, Async 접두사 별칭·aio 팩터리·동기 네트워크 경로를 추가하지 않는다. JSON/XML `resultCode=03`은 최신 계약에 따라 빈 결과로 정규화한다. + `python-kma-api`는 기상청 공공 날씨 API를 위한 Python 클라이언트이며 import package 이름은 `kma`입니다. public 동작을 바꾸기 전 `README.md`, `kma-api.md`, `docs/api-coverage.md`, `docs/apihub.md`, `docs/apihub-endpoints.md`, `docs/datagokr.md`, `AGENTS.md`를 확인합니다. ## 프로젝트 불변조건 @@ -20,7 +22,7 @@ description: 기상청 공공 날씨 API용 Python 클라이언트를 구현, 9. 붙여넣은 인증키 공백은 클라이언트 경계에서 제거하고, `.env`/`.env.local` 로컬 키 로딩을 지원합니다. 10. KMA 예보 시간은 KST(UTC+9)입니다. naive `datetime`은 KST로 해석합니다. 11. public API는 `location=LatLon(...)`, `location=GridPoint(...)`, WGS84 `lat`/`lon`, KMA 격자 `nx`/`ny` 중 하나를 받습니다. `nx`/`ny`를 위도/경도로 취급하지 않습니다. -12. KMA `resultCode != "00"`은 typed exception으로 surface합니다. +12. KMA `resultCode`가 "00"/"03" 이외인 경우는 typed exception으로 surface합니다. 13. 기본 테스트는 실제 KMA/APIHub API를 호출하지 않습니다. 14. APIHub 응답은 JSON, XML, 텍스트, 이미지, 바이너리가 섞여 있으므로 하나의 모델로 강제하지 않습니다. 15. APIHub legacy 그래픽 URL의 이름 없는 query string은 순서가 의미이므로 `arg1`, `arg2` 순서를 보존합니다. @@ -92,7 +94,7 @@ tests/ ### `KmaClient` -```python +```text KmaClient(service_key, *, timeout=10, retries=3, base_url=None, session=None) KmaClient.from_env(name="DATA_GO_KR_SERVICE_KEY") ``` @@ -100,12 +102,22 @@ KmaClient.from_env(name="DATA_GO_KR_SERVICE_KEY") 위치 인자는 다음 형식 중 하나만 받습니다. ```python -kma.now(location=LatLon(37.5665, 126.9780)) -kma.now(location=GridPoint(60, 127)) -kma.now(location={"latitude": 37.5665, "longitude": 126.9780}) -kma.now(location={"nx": 60, "ny": 127}) -kma.now(lat=37.5665, lon=126.9780) -kma.now(nx=60, ny=127) +from kma import LatLon, GridPoint +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + (await kma.now(location=LatLon(37.5665, 126.9780))) + (await kma.now(location=GridPoint(60, 127))) + (await kma.now(location={"latitude": 37.5665, "longitude": 126.9780})) + (await kma.now(location={"nx": 60, "ny": 127})) + (await kma.now(lat=37.5665, lon=126.9780)) + (await kma.now(nx=60, ny=127)) + + +asyncio.run(main()) ``` 거부해야 하는 입력: @@ -127,9 +139,18 @@ kma.now(nx=60, ny=127) ### `DataGoKrClient` ```python -DataGoKrClient(service_key) -client.request("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}) -client.items("MidFcstInfoService", "getMidFcst", {...}) +from kma import latest_mid_fcst_time +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + (await client.request("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"})) + (await client.items("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": latest_mid_fcst_time()})) + + +asyncio.run(main()) ``` 규칙: @@ -142,9 +163,17 @@ client.items("MidFcstInfoService", "getMidFcst", {...}) ### `ApiHubClient` ```python -ApiHubClient(auth_key) -hub.request_path("/api/typ01/url/wrn_reg.php", {"tmfc": "0"}) -hub.open_api("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}) +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + (await hub.request_path("/api/typ01/url/wrn_reg.php", {"tmfc": "0"})) + (await hub.open_api("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"})) + + +asyncio.run(main()) ``` 규칙: @@ -158,11 +187,17 @@ hub.open_api("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "20260 ### `ApiHubGeneratedClient` ```python +import asyncio from kma import ApiHubGeneratedClient -hub = ApiHubGeneratedClient.from_env() -hub.kma_sfctm2(tm="202605010900", stn="108", help="1") -hub.aws3_nph_awsm_tms_h06(use_sample=True) + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + (await hub.kma_sfctm2(tm="202605010900", stn="108", help="1")) + (await hub.aws3_nph_awsm_tms_h06(use_sample=True)) + + +asyncio.run(main()) ``` 규칙: @@ -306,7 +341,7 @@ KmaError | 코드 | 처리 | |---|---| | `00` | 성공 | -| `03` | `KmaRequestError` | +| `03` | 정상 빈 결과 | | `04` | `KmaServerError` | | `12` | `KmaRequestError` | | `20` | `KmaAuthError` | diff --git a/docs/api-coverage.md b/docs/api-coverage.md index a3ad688..03176e6 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -43,7 +43,16 @@ http://apis.data.go.kr/1360000/{service}/{operation} 예: ```python -client.request("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}) +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + (await client.request("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"})) + + +asyncio.run(main()) ``` 이 계층은 특정 endpoint를 개별 모델로 구현한 것이 아니라, 표준 data.go.kr envelope를 범용으로 처리합니다. 따라서 “개별 구현 endpoint 개수”에는 넣지 않습니다. @@ -101,13 +110,31 @@ https://apihub.kma.go.kr/api/... 예: ```python -hub.request_path("/api/typ01/url/wrn_reg.php", {"tmfc": "0"}) +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + (await hub.request_path("/api/typ01/url/wrn_reg.php", {"tmfc": "0"})) + + +asyncio.run(main()) ``` 또한 `typ02/openApi` helper를 제공합니다. ```python -hub.open_api("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}) +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + (await hub.open_api("MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"})) + + +asyncio.run(main()) ``` APIHub는 텍스트, JSON, XML, 이미지, 바이너리 파일 응답이 섞여 있습니다. `kma`는 endpoint별 반환 스키마를 모두 Pydantic 모델로 고정하지는 않지만, 공식 목록에서 확인한 endpoint를 `ApiHubGeneratedClient`의 함수형 메서드로 제공합니다. @@ -115,10 +142,16 @@ APIHub는 텍스트, JSON, XML, 이미지, 바이너리 파일 응답이 섞여 예: ```python +import asyncio from kma import ApiHubGeneratedClient -hub = ApiHubGeneratedClient.from_env() -response = hub.kma_sfctm2(tm="202605010900", stn="108", help="1") + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.kma_sfctm2(tm="202605010900", stn="108", help="1")) + + +asyncio.run(main()) ``` 전체 목록은 [docs/apihub-endpoints.md](apihub-endpoints.md)에 있습니다. diff --git a/docs/apihub-endpoints.md b/docs/apihub-endpoints.md index 1920ce1..a14a411 100644 --- a/docs/apihub-endpoints.md +++ b/docs/apihub-endpoints.md @@ -21,24 +21,48 @@ ## 사용법 ```python +import asyncio from kma import ApiHubGeneratedClient -hub = ApiHubGeneratedClient.from_env() -response = hub.kma_sfctm2(tm="202605010900", stn="108", help="1") -rows = response.text_table().rows + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.kma_sfctm2(tm="202605010900", stn="108", help="1")) + rows = response.text_table().rows + + +asyncio.run(main()) ``` 홈페이지 예제 값을 그대로 써서 호출하려면 `use_sample=True`를 넘깁니다. 실제 운영 코드에서는 예제 날짜가 오래되었을 수 있으므로 필요한 인자를 명시하는 것을 권장합니다. ```python -response = hub.kma_sfctm2(use_sample=True, stn="108") +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.kma_sfctm2(use_sample=True, stn="108")) + + +asyncio.run(main()) ``` 이미지 endpoint는 bytes와 포맷/크기 정보를 함께 얻을 수 있습니다. ```python -image = hub.image_endpoint("api_iwa_img_url_api_ret_grid_img", use_sample=True) -print(image.format, image.width, image.height) +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + image = (await hub.image_endpoint("api_iwa_img_url_api_ret_grid_img", use_sample=True)) + print(image.format, image.width, image.height) + + +asyncio.run(main()) ``` 이름 없는 query string을 쓰는 legacy 그래픽 URL은 `arg1`, `arg2`처럼 순서형 인자로 노출합니다. 예를 들어 `?202305031000&0&...` 형태는 `arg1="202305031000"`, `arg2="0"`로 넘깁니다. diff --git a/docs/apihub.md b/docs/apihub.md index 9a04ea6..c0cb0f0 100644 --- a/docs/apihub.md +++ b/docs/apihub.md @@ -123,16 +123,22 @@ APIHub는 같은 포털 안에서도 응답 형식이 크게 다릅니다. `kma` ## 기본 사용 ```python +import asyncio from kma import ApiHubGeneratedClient -hub = ApiHubGeneratedClient.from_env() # KMA_APIHUB_AUTH_KEY 또는 KMA_APIHUB_KEY -response = hub.kma_sfctm2( - tm="202605010900", - stn="108", - help="1", -) -print(response.text) +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + + response = (await hub.kma_sfctm2( + tm="202605010900", + stn="108", + help="1", + )) + print(response.text) + + +asyncio.run(main()) ``` 실제 서버 테스트용 인증키는 `.env.local`에 보관할 수 있습니다. 이 파일은 `.gitignore`에 포함되어 커밋되지 않습니다. @@ -146,7 +152,16 @@ KMA_APIHUB_AUTH_KEY= 홈페이지 예제 값을 채워 호출하려면 `use_sample=True`를 사용합니다. 예제 날짜가 오래되었을 수 있으므로 운영 코드에서는 필요한 인자를 직접 넘기는 방식을 권장합니다. ```python -response = hub.kma_sfctm2(use_sample=True, stn="108") +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.kma_sfctm2(use_sample=True, stn="108")) + + +asyncio.run(main()) ``` ## Endpoint metadata @@ -172,12 +187,21 @@ print(spec.sample_params) 일반 TXT endpoint는 `ApiHubResponse.text_table()`로 주석, header, row를 분리할 수 있습니다. ```python -response = hub.kma_sfctm2(tm="202605010900", stn="108", help="1") -table = response.text_table() +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.kma_sfctm2(tm="202605010900", stn="108", help="1")) + table = response.text_table() -print(table.headers) -print(table.rows[:3]) -print(table.comments[:3]) + print(table.headers) + print(table.rows[:3]) + print(table.comments[:3]) + + +asyncio.run(main()) ``` CSV식 응답은 delimiter를 지정합니다. @@ -193,9 +217,18 @@ TXT 포맷은 endpoint마다 완전히 같지 않습니다. header를 안정적 이미지/그래픽 endpoint는 `image_endpoint()` 또는 `response.image()`를 사용합니다. ```python -image = hub.image_endpoint("api_iwa_img_url_api_ret_grid_img", use_sample=True) -print(image.format, image.width, image.height) -content = image.content +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + image = (await hub.image_endpoint("api_iwa_img_url_api_ret_grid_img", use_sample=True)) + print(image.format, image.width, image.height) + content = image.content + + +asyncio.run(main()) ``` `image.format`, `image.width`, `image.height`는 PNG/GIF/JPEG header에서 감지합니다. 포맷을 알 수 없는 바이너리는 `None`으로 둡니다. @@ -221,15 +254,24 @@ for attachment in APIHUB_ATTACHMENTS: 이런 endpoint는 `arg1`, `arg2`처럼 순서형 인자로 래핑했습니다. ```python -response = hub.aws3_nph_awsm_tms_h06( - arg1="202305031000", - arg2="0", - arg3="108,419", - arg4="m", - arg5="108,419", - arg6="kh", - _DT="RSW:AWSCHART", -) +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.aws3_nph_awsm_tms_h06( + arg1="202305031000", + arg2="0", + arg3="108,419", + arg4="m", + arg5="108,419", + arg6="kh", + _DT="RSW:AWSCHART", + )) + + +asyncio.run(main()) ``` `ApiHubGeneratedClient`는 이 경우 `requests`의 `params=`를 쓰지 않고 query string을 직접 조립해 순서를 보존합니다. @@ -239,24 +281,39 @@ response = hub.aws3_nph_awsm_tms_h06( 목록에 없는 새 endpoint나 실험적 path는 `request_path()`로 직접 호출합니다. ```python +import asyncio from kma import ApiHubClient -hub = ApiHubClient.from_env() -response = hub.request_path( - "/api/typ01/url/wrn_reg.php", - {"tmfc": "0"}, -) + +async def main() -> None: + async with ApiHubClient.from_env() as hub: + response = (await hub.request_path( + "/api/typ01/url/wrn_reg.php", + {"tmfc": "0"}, + )) + + +asyncio.run(main()) ``` `typ02/openApi` 형식은 `open_api()` helper를 사용할 수 있습니다. ```python -response = hub.open_api( - "MidFcstInfoService", - "getMidFcst", - {"stnId": "108", "tmFc": "202605010600"}, -) -data = response.json() +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.open_api( + "MidFcstInfoService", + "getMidFcst", + {"stnId": "108", "tmFc": "202605010600"}, + )) + data = response.json() + + +asyncio.run(main()) ``` 기본값: @@ -270,13 +327,31 @@ data = response.json() `discover_services()`는 공식 분류 id별 APIHub 서비스 목록을 가져옵니다. ```python -services = hub.discover_services() +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + services = (await hub.discover_services()) + + +asyncio.run(main()) ``` `discover_endpoints()`는 서비스 페이지의 예제 URL을 추출합니다. ```python -endpoints = hub.discover_endpoints(category_id=10, service_id=288) +from kma import ApiHubGeneratedClient +import asyncio + + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + endpoints = (await hub.discover_endpoints(category_id=10, service_id=288)) + + +asyncio.run(main()) ``` 탐색 기능은 live 포털 구조를 읽는 도구입니다. 패키지에 포함된 함수형 래퍼는 `tools/update_apihub_endpoints.py`로 생성한 고정 snapshot입니다. diff --git a/docs/async-tps.md b/docs/async-tps.md new file mode 100644 index 0000000..6774ff6 --- /dev/null +++ b/docs/async-tps.md @@ -0,0 +1,47 @@ +# 비동기 API와 공통 TPS + +KmaClient, DataGoKrClient, ApiHubClient, ApiHubGeneratedClient는 비동기 전용이다. +네트워크 호출·디버그에는 await, 페이지 순회에는 async for, 종료에는 async with 또는 +await client.aclose()를 사용한다. Async 접두사 클래스와 aio 팩터리, a 접두사 메서드는 제거했다. +기존 일반 메서드 이름을 그대로 await한다. ForecastService의 호출과 now/short/vilage/version도 async다. + +```python +import asyncio +from kma import AsyncTokenBucket, KmaClient, DataGoKrClient, latest_mid_fcst_time + + +async def main() -> None: + budget = AsyncTokenBucket(2, capacity=1) + async with ( + KmaClient.from_env(rate_limiter=budget) as weather, + DataGoKrClient.from_env(rate_limiter=budget) as client, + ): + print(await weather.now(nx=60, ny=127)) + async for page in (client.iter_pages( + "MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": latest_mid_fcst_time()}, num_of_rows=10, + )): + print(page) + + +asyncio.run(main()) +``` + +기본 max_rps=5.0, 기본 capacity=max(1, max_rps)는 초기 burst를 허용한다. +일정한 송신 간격은 capacity=1을 지정한다. rate_limiter를 주입하면 max_rps보다 우선하며, +여러 클라이언트가 같은 버킷을 공유하면 합산 TPS가 제한된다. 한 이벤트 루프에서 사용하며, +대기 취소는 토큰을 소비하지 않는다. 공급자의 일일 사용량 제한은 별도다. + +일반·타입화 조회, 디버그, 페이지, APIHub 470개 생성 메서드, 탐색 호출은 같은 경로를 쓴다. +첫 송신·재시도·각 리다이렉트마다 한 토큰을 소비한다. 기존 429/500/502/503/504 및 일시적인 +연결·timeout 재시도와 jitter/Retry-After를 유지하며, 401/403과 resultCode=22는 즉시 실패한다. +HTTPX의 BasicAuth/기본 인증 흐름을 지원하고 추가 송신을 숨기는 Digest/custom Auth는 거부한다. +사용자 정의 transport 내부의 추가 송신은 해당 transport의 책임이다. + +내부 세션은 첫 요청에 만들고 재사용하며 aclose가 닫는다. 주입한 비동기 세션은 호출자가 +닫는다. 토큰 대기 중 종료되거나 인증 흐름이 바뀌면 송신 전에 다시 검사한다. +공개 오류와 DebugRun의 모든 필드에서 실제 키와 인코딩된 키를 마스킹한다. + +일반 함수로 남는 것은 좌표·시간·코드표·모델·카탈로그·응답 파싱·fixture 저장 등 로컬 작업이다. +JSON/XML NO_DATA03 빈 결과, quota22 비재시도, tmFc 요청값 폴백, PCP/SNO 범주 문자열, +페이지 상한 경고와 APIHub bare query 순서를 유지한다. CLI와 Streamlit은 진입 경계에서만 +asyncio.run을 사용하고 종료 시 세션을 닫는다. 생성기 역시 비동기 HTTP와 공통 TPS를 사용한다. diff --git a/docs/datagokr.md b/docs/datagokr.md index cfe63df..93a1e88 100644 --- a/docs/datagokr.md +++ b/docs/datagokr.md @@ -14,12 +14,18 @@ ## 타입화 클라이언트 ```python +import asyncio from kma import KmaClient -kma = KmaClient.from_env() -kma.now(nx=60, ny=127) -kma.forecast_short(nx=60, ny=127) -kma.forecast(nx=60, ny=127) + +async def main() -> None: + async with KmaClient.from_env() as kma: + (await kma.now(nx=60, ny=127)) + (await kma.forecast_short(nx=60, ny=127)) + (await kma.forecast(nx=60, ny=127)) + + +asyncio.run(main()) ``` 타입화 클라이언트가 다루는 endpoint: @@ -32,34 +38,58 @@ kma.forecast(nx=60, ny=127) ## 범용 클라이언트 ```python +import asyncio from kma import DataGoKrClient -client = DataGoKrClient.from_env() -body = client.request( - "MidFcstInfoService", - "getMidFcst", - {"stnId": "108", "tmFc": "202605010600"}, -) + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + body = (await client.request( + "MidFcstInfoService", + "getMidFcst", + {"stnId": "108", "tmFc": "202605010600"}, + )) + + +asyncio.run(main()) ``` 표준 `response.body.items.item` 구조를 쓰는 operation은 `items()`를 사용할 수 있습니다. ```python -items = client.items( - "MidFcstInfoService", - "getMidFcst", - {"stnId": "108", "tmFc": "202605010600"}, -) +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + items = (await client.items( + "MidFcstInfoService", + "getMidFcst", + {"stnId": "108", "tmFc": "202605010600"}, + )) + + +asyncio.run(main()) ``` metadata가 필요하면 `request_with_metadata()`를 사용합니다. metadata의 `request_params`에는 `serviceKey` 원문이 없습니다. ```python -body, metadata = client.request_with_metadata( - "MidFcstInfoService", - "getMidFcst", - {"stnId": "108", "tmFc": "202605010600"}, -) +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + body, metadata = (await client.request_with_metadata( + "MidFcstInfoService", + "getMidFcst", + {"stnId": "108", "tmFc": "202605010600"}, + )) + + +asyncio.run(main()) ``` ## 기상청 API 카탈로그 @@ -69,24 +99,30 @@ body, metadata = client.request_with_metadata( 카탈로그 86개 중 기존 data.go.kr `serviceKey` gateway 항목은 38개이며, 포털 상세기능에서 확인한 operation 160개를 함께 보존합니다. APIHub로 연결되는 48개 항목은 `gateway="apihub"`로 구분합니다. APIHub와 정확히 같은 `{service}/{operation}` 조합은 [data.go.kr/APIHub 중복 확인](datagokr-apihub-overlap.md)에 표로 정리했습니다. ```python +import asyncio from kma import KMA_DATA_GOKR_DATASETS, DataGoKrClient, api_catalog -client = DataGoKrClient.from_env() - -print(len(KMA_DATA_GOKR_DATASETS)) # 86 -for entry in api_catalog(gateway="datagokr")[:3]: - print(entry.dataset_name, entry.operation, entry.service_key_url) - -asos_spec = client.dataset("15059093") -rows = client.dataset_items( - "15059093", - { - "startDt": "20260501", - "endDt": "20260502", - "dataCd": "ASOS", - "dateCd": "DAY", - }, -) + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + + print(len(KMA_DATA_GOKR_DATASETS)) # 86 + for entry in api_catalog(gateway="datagokr")[:3]: + print(entry.dataset_name, entry.operation, entry.service_key_url) + + asos_spec = client.dataset("15059093") + rows = (await client.dataset_items( + "15059093", + { + "startDt": "20260501", + "endDt": "20260502", + "dataCd": "ASOS", + "dateCd": "DAY", + }, + )) + + +asyncio.run(main()) ``` `api_catalog()`는 dataset을 operation 단위 row로 펼쳐 `dataset_name`, `label`, `gateway`, `service`, `operation`, `credential_param`, `service_key_url`을 제공합니다. Streamlit 같은 디버그 UI에서는 `label`을 선택 항목으로 쓰고, 사용자가 선택한 row의 `service_key_url`을 서비스키 발급/확인 링크로 보여주면 됩니다. @@ -94,11 +130,20 @@ rows = client.dataset_items( `dataset_items()`는 서비스와 operation이 하나로 결정되는 data.go.kr gateway 항목을 바로 호출합니다. 여러 operation을 가진 항목은 `operation=`을 명시합니다. ```python -waves = client.dataset_items( - "15102239", - {"beach_num": "1", "searchTime": "202205011600"}, - operation="getWhBuoyBeach", -) +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + waves = (await client.dataset_items( + "15102239", + {"beach_num": "1", "searchTime": "202205011600"}, + operation="getWhBuoyBeach", + )) + + +asyncio.run(main()) ``` 카탈로그에는 APIHub로 연결되는 data.go.kr 항목도 `gateway="apihub"`로 남겨 둡니다. 이 항목은 `serviceKey` gateway가 아니므로 `dataset_items()`가 호출하지 않고, `ApiHubClient` 또는 `ApiHubGeneratedClient`를 사용해야 합니다. @@ -123,24 +168,42 @@ client = DataGoKrClient.from_env(service_key_param="ServiceKey") data.go.kr 계열 response body가 `pageNo`, `numOfRows`, `totalCount`를 포함하면 다음 helper를 사용할 수 있습니다. ```python +from kma import latest_mid_fcst_time +from kma import DataGoKrClient +import asyncio from kma import has_next_page, next_page_no -body = client.request("MidFcstInfoService", "getMidLandFcst", {...}) -if has_next_page(body): - print(next_page_no(body)) + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + body = (await client.request("MidFcstInfoService", "getMidLandFcst", {"regId": "11B00000", "tmFc": latest_mid_fcst_time()})) + if has_next_page(body): + print(next_page_no(body)) + + +asyncio.run(main()) ``` `DataGoKrClient.iter_pages()`는 `max_pages` 또는 `max_items` guard로 무한 반복을 방지합니다. ```python -for body in client.iter_pages( - "MidFcstInfoService", - "getMidLandFcst", - {"regId": "11B00000", "tmFc": "202605010600"}, - num_of_rows=100, - max_pages=10, -): - ... +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + async for body in (client.iter_pages( + "MidFcstInfoService", + "getMidLandFcst", + {"regId": "11B00000", "tmFc": "202605010600"}, + num_of_rows=100, + max_pages=10, + )): + ... + + +asyncio.run(main()) ``` ## 중기예보 helper @@ -148,11 +211,20 @@ for body in client.iter_pages( 중기예보는 `MidFcstInfoService` 호출과 row parsing까지만 책임집니다. `reg_id`는 단기예보 `nx`/`ny`와 다른 KMA 중기예보 권역 코드이며, `kma`는 임의 매핑을 추측하지 않습니다. `tm_fc`를 생략하면 06:00/18:00 발표와 10분 조회 지연을 반영한 최신 `tmFc`를 사용합니다. ```python -client.mid_forecast(stn_id="108", tm_fc="202605010600") -client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600") -client.mid_land_forecast(reg_id="11B00000") # 최신 조회 가능 tmFc 자동 선택 -client.mid_temperature_forecast(reg_id="11B10101", tm_fc="202605010600") -client.mid_sea_forecast(reg_id="12A20000", tm_fc="202605010600") +from kma import DataGoKrClient +import asyncio + + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + (await client.mid_forecast(stn_id="108", tm_fc="202605010600")) + (await client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600")) + (await client.mid_land_forecast(reg_id="11B00000")) # 최신 조회 가능 tmFc 자동 선택 + (await client.mid_temperature_forecast(reg_id="11B10101", tm_fc="202605010600")) + (await client.mid_sea_forecast(reg_id="12A20000", tm_fc="202605010600")) + + +asyncio.run(main()) ``` 각 row는 `MidForecastItem`이며 `operation`, `tm_fc`, `reg_id`, `stn_id`, `raw`, `metadata`를 제공합니다. @@ -164,40 +236,46 @@ client.mid_sea_forecast(reg_id="12A20000", tm_fc="202605010600") 2026-05-07에 공공데이터포털 `기상청` 오픈 API 검색에서 확인한 주요 data.go.kr 서비스는 전용 helper를 제공합니다. endpoint별 안정적인 도메인 모델을 확정하기 어려운 서비스는 `DataGoKrItem`으로 감싸며, 각 row의 `raw`와 인증키가 제거된 `metadata`를 보존합니다. ```python +import asyncio from kma import DataGoKrClient -client = DataGoKrClient.from_env() - -daily = client.asos_daily_weather( - start_dt="20260501", - end_dt="20260502", - stn_ids=108, -) -hourly = client.asos_hourly_weather( - start_dt="20260501", - start_hh="00", - end_dt="20260501", - end_hh="23", - stn_ids=108, -) -warnings = client.weather_warning_list( - stn_id=108, - from_tm_fc="20260501", - to_tm_fc="20260502", -) -situation = client.weather_situation(stn_id=108) -land = client.land_forecast_message(reg_id="11B10101") -sea = client.sea_forecast_message(reg_id="12A20100") -tour = client.tour_village_forecast(course_id=1, current_date="20260501", hour="09") -climate = client.city_tour_climate_index(city_area_id=1100000000, current_date="20260501", day=3) -uv = client.uv_index(area_no="1100000000", time="2026050106") -air = client.air_diffusion_index(area_no="1100000000", time="2026050106") -sen = client.sensible_temperature_index( - area_no="1100000000", - time="2026050106", - request_code="A41", -) -quake = client.earthquake_message_list(from_tm_fc="20260501", to_tm_fc="20260502") + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + + daily = (await client.asos_daily_weather( + start_dt="20260501", + end_dt="20260502", + stn_ids=108, + )) + hourly = (await client.asos_hourly_weather( + start_dt="20260501", + start_hh="00", + end_dt="20260501", + end_hh="23", + stn_ids=108, + )) + warnings = (await client.weather_warning_list( + stn_id=108, + from_tm_fc="20260501", + to_tm_fc="20260502", + )) + situation = (await client.weather_situation(stn_id=108)) + land = (await client.land_forecast_message(reg_id="11B10101")) + sea = (await client.sea_forecast_message(reg_id="12A20100")) + tour = (await client.tour_village_forecast(course_id=1, current_date="20260501", hour="09")) + climate = (await client.city_tour_climate_index(city_area_id=1100000000, current_date="20260501", day=3)) + uv = (await client.uv_index(area_no="1100000000", time="2026050106")) + air = (await client.air_diffusion_index(area_no="1100000000", time="2026050106")) + sen = (await client.sensible_temperature_index( + area_no="1100000000", + time="2026050106", + request_code="A41", + )) + quake = (await client.earthquake_message_list(from_tm_fc="20260501", to_tm_fc="20260502")) + + +asyncio.run(main()) ``` 지원 범위: @@ -220,20 +298,26 @@ quake = client.earthquake_message_list(from_tm_fc="20260501", to_tm_fc="20260502 (`BeachInfoservice`)는 `DataGoKrClient`의 전용 helper로 호출할 수 있습니다. ```python +import asyncio from kma import DataGoKrClient -client = DataGoKrClient.from_env() - -ultra = client.beach_ultra_short_forecast( - beach_num=1, - base_date="20220622", - base_time="1230", -) -forecast = client.beach_forecast(beach_num=1) -waves = client.beach_wave_height(beach_num=1, search_time="202205011600") -tides = client.beach_tide_info(beach_num=1, base_date="20220620") -sun = client.beach_sun_info(beach_num=1, base_date="20220501") -water = client.beach_water_temperature(beach_num=1, search_time="202205011600") + +async def main() -> None: + async with DataGoKrClient.from_env() as client: + + ultra = (await client.beach_ultra_short_forecast( + beach_num=1, + base_date="20220622", + base_time="1230", + )) + forecast = (await client.beach_forecast(beach_num=1)) + waves = (await client.beach_wave_height(beach_num=1, search_time="202205011600")) + tides = (await client.beach_tide_info(beach_num=1, base_date="20220620")) + sun = (await client.beach_sun_info(beach_num=1, base_date="20220501")) + water = (await client.beach_water_temperature(beach_num=1, search_time="202205011600")) + + +asyncio.run(main()) ``` 지원 operation: @@ -266,7 +350,7 @@ DATA_GO_KR_SERVICE_KEY= ## HTTP 200 XML 오류 envelope `dataType=JSON` 요청이어도 data.go.kr gateway는 quota·인증 오류를 HTTP 200 -`OpenAPI_ServiceResponse` XML로 반환할 수 있습니다. `DataGoKrClient`의 동기·비동기 +`OpenAPI_ServiceResponse` XML로 반환할 수 있습니다. `DataGoKrClient`의 비동기 호출은 JSON parse 실패 때 XML의 `returnReasonCode`/`resultCode`를 공통 result-code 정책으로 분류합니다. `03`은 빈 `items`/`totalCount=0`, `22`는 `KmaRequestError(failure_kind="quota", retryable=False)`이며, XML이 아니거나 코드가 diff --git a/docs/decisions.md b/docs/decisions.md index 509b3a5..a26332a 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -4,7 +4,7 @@ ## ADR-001: httpx 기반 HTTP 클라이언트 -- **상태**: 확정 +- **상태**: [SUPERSEDED by ADR-006] 동기/비동기 공존 부분. httpx 선택은 유지한다. - **결정**: `requests` 대신 `httpx`를 사용한다. - **이유**: sync/async 통합 API, `follow_redirects` 기본 지원, `params=` 인코딩 동작이 data.go.kr serviceKey 전달에 적합. - **결과**: `_http.py`에서 `httpx.Client`/`httpx.AsyncClient`를 생성하고, `get_with_retries`/`async_get_with_retries`로 재시도를 처리한다. @@ -36,3 +36,12 @@ - **결정**: `_float_or_none`, `_int_or_none`, `_str_or_none`을 `_parsing.py` 공유 모듈로 추출한다. - **이유**: `client.py`와 `datagokr.py`에 동일 구현이 중복되어 있었다. - **결과**: 양쪽 모듈에서 import로 대체. + + +## ADR-006: 비동기 전용 클라이언트와 공통 TPS (2026-09-14) + +사용자 요청에 따라 ADR-001의 동기/비동기 공존 부분을 대체한다. 기존 async 구현을 공개 +클라이언트의 일반 메서드 이름으로 통합하고 동기 네트워크 코드와 Async/aio 별칭을 제거한다. +다른 API 라이브러리와 같은 AsyncTokenBucket 로직을 적용해 재시도·리다이렉트도 합산 제한한다. +파싱·모델·카탈로그와 CLI/UI 진입점은 일반 함수로 유지한다. 기존 동기 소비자는 await, +async for, async with로 전환해야 한다. 자세한 설정은 async-tps.md에 기록한다. diff --git a/docs/journal.md b/docs/journal.md index 50d72c5..8612ebb 100644 --- a/docs/journal.md +++ b/docs/journal.md @@ -1,5 +1,12 @@ # JOURNAL — 작업 일지 +## 2026-09-14 — 비동기 전용 API와 공통 TPS + +- 원격 최신 main 기준으로 Kma/DataGoKr/ApiHub와 생성 endpoint 470개를 비동기로 통합했다. +- 공통 버킷으로 첫 송신·재시도·redirect·탐색·디버그·페이지를 제한하고 취소/종료를 검증했다. +- CLI/UI와 실제 생성기의 예제를 갱신했다. 검증 및 두 리뷰 결과는 verification-async-tps.md에 기록한다. + + 새 항목은 항상 파일 맨 위에 추가(역시간순). 기존 항목은 절대 수정하지 않는다 — 잘못된 결정조차 기록으로 남는 것이 가치다. ## 2026-09-11 (claude, asyncio 재검증 2인 적대적 리뷰 — ApiHubClient.aiter_pages 수정) diff --git a/docs/repeated-mistakes.md b/docs/repeated-mistakes.md index 6712785..c54500a 100644 --- a/docs/repeated-mistakes.md +++ b/docs/repeated-mistakes.md @@ -87,7 +87,7 @@ **증상:** 사용자가 KMA의 중첩 구조인 `response.header.body.items.item`을 직접 이해해야 합니다. -**규칙:** 잘못된 envelope/item은 `KmaParseError`로, `resultCode != "00"`은 typed KMA exception으로 변환합니다. +**규칙:** 잘못된 envelope/item은 `KmaParseError`로, `resultCode`가 "00"/"03" 이외인 경우는 typed KMA exception으로 변환합니다. **방지 테스트:** 잘못된 envelope, 누락된 `items`, 잘못된 forecast item, 단일 dict 응답, result code 매핑을 테스트합니다. diff --git a/docs/resume.md b/docs/resume.md index c8193eb..d8f51e7 100644 --- a/docs/resume.md +++ b/docs/resume.md @@ -1,8 +1,10 @@ # RESUME — 작업 재개 가이드 +2026-09-14: 비동기 전용 및 공통 TPS 전환 완료. 검증/실제 API 결과는 verification-async-tps.md에 기록한다. + 새 에이전트 세션이 시작될 때 "지금 어디까지 했고, 다음은 뭐 하면 되나"를 한 화면에서 답한다. -## 현재 진척도 (2026-09-11 갱신) +## 현재 진척도 (2026-09-14 갱신) - ✅ Windows 기준 고정 worktree alias 복구 및 `.codegraph/` Git 상태 노이즈 제거 - ✅ `KmaClient` 타입화 단기예보 4개 endpoint (`getUltraSrtNcst`, `getUltraSrtFcst`, `getVilageFcst`, `getFcstVersion`) @@ -18,13 +20,13 @@ - ✅ 예외 계층 (`KmaError` → `Auth`/`Request`/`Server`/`Parse`) - ✅ 인증값 보안 (redaction, sanitize, `.env` 로딩) - ✅ 165개 테스트 (153 mock + 12 live, 라이브는 키 구독에 따라 일부 skip), ruff/mypy 통과 -- ✅ httpx async facade (`build_session`, `build_async_client`, sync/async retry) +- ✅ httpx 비동기 전용 세션과 공통 토큰 버킷 - ✅ `_parsing.py` 공유 파싱 도우미 추출 (PR #3) - ✅ `maplibre-vworld-js` 에이전트 스타일, 고정 worktree 규칙, AI용 가이드 문서, MCP 설정 도입 및 PR 머지 완료 - ✅ 에이전트 고정 워크트리(`python-kma-api-*`) 실제 생성 및 CodeGraph 색인(`codegraph init -i`) 완료 - ✅ HTTP 에러 핸들링 공통 추출 — `raise_for_kma_http_error()` / `raise_for_kma_network_error()` (T-001, 6곳 통합) - ✅ result code 핸들링 통합 — `raise_for_kma_result_code()` (T-002) -- ✅ async 패턴 일관화 — `AsyncDataGoKrClient`/`AsyncApiHubClient` facade, `aio()` 반환 변경 (T-003) +- ✅ async 패턴 일관화 — 공개 클라이언트에 비동기 메서드 통합, 별도 facade 제거 (T-003) - ✅ ASOS helper 전용 Pydantic 모델 — `AsosDailyItem`/`AsosHourlyItem` (T-004) - ✅ 특보 전용 Pydantic 모델 — `WeatherWarningItem`, `weather_warning_list()` 적용 (T-005) - ✅ retry에 jitter 추가 — `_backoff_with_jitter()` equal jitter (T-006) diff --git a/docs/testing.md b/docs/testing.md index 4284007..2f05ac3 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -116,11 +116,17 @@ python -m pytest -m "not integration" 함수형 래퍼를 직접 smoke test할 때: ```python +import asyncio from kma import ApiHubGeneratedClient -hub = ApiHubGeneratedClient.from_env() -response = hub.kma_sfctm2(tm="202605010900", stn="108", help="1") -print(response.text[:200]) + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.kma_sfctm2(tm="202605010900", stn="108", help="1")) + print(response.text[:200]) + + +asyncio.run(main()) ``` 이 테스트도 인증키가 필요하므로 기본 테스트에 넣지 않습니다. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3ecd621..00ddb63 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -37,10 +37,19 @@ `KmaError` 하위 예외에는 선택적 metadata가 있습니다. ```python -try: - kma.now(nx=60, ny=127) -except KmaError as exc: - print(exc.failure_kind, exc.retryable, exc.metadata) +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + try: + (await kma.now(nx=60, ny=127)) + except KmaError as exc: + print(exc.failure_kind, exc.retryable, exc.metadata) + + +asyncio.run(main()) ``` 분류 기준: @@ -133,10 +142,18 @@ kma now --lat 37.5665 --lon 126.9780 --nx 60 --ny 127 Python 코드에서는 좌표계를 명확히 하기 위해 값 객체를 사용할 수 있습니다. ```python +from kma import KmaClient +import asyncio from kma import GridPoint, LatLon -kma.now(location=LatLon(37.5665, 126.9780)) -kma.now(location=GridPoint(60, 127)) + +async def main() -> None: + async with KmaClient.from_env() as kma: + (await kma.now(location=LatLon(37.5665, 126.9780))) + (await kma.now(location=GridPoint(60, 127))) + + +asyncio.run(main()) ``` 앱 저장 경계에서 `latitude`/`longitude` 이름을 쓴다면 명시적 alias를 사용할 수 있습니다. @@ -189,10 +206,16 @@ pip install requests - 순서형 값은 `arg1`, `arg2`로 넘깁니다. ```python +import asyncio from kma import ApiHubGeneratedClient -hub = ApiHubGeneratedClient.from_env() -response = hub.aws3_nph_awsm_tms_h06(use_sample=True) + +async def main() -> None: + async with ApiHubGeneratedClient.from_env() as hub: + response = (await hub.aws3_nph_awsm_tms_h06(use_sample=True)) + + +asyncio.run(main()) ``` ## APIHub 함수 이름을 찾기 어려움 @@ -224,8 +247,17 @@ APIHub와 data.go.kr는 서로 다른 gateway입니다. typed 모델은 `raw`와 sanitized `metadata`를 함께 제공합니다. ```python -snapshot = kma.now(nx=60, ny=127) -payload = snapshot.model_dump(mode="json") +from kma import KmaClient +import asyncio + + +async def main() -> None: + async with KmaClient.from_env() as kma: + snapshot = (await kma.now(nx=60, ny=127)) + payload = snapshot.model_dump(mode="json") + + +asyncio.run(main()) ``` 이 결과를 그대로 raw 저장에 쓰거나, 앱에서 필요한 필드만 골라 serving 저장에 사용할 수 있습니다. `metadata.request_params`에는 인증키 원문이 없습니다. diff --git a/docs/verification-async-tps.md b/docs/verification-async-tps.md new file mode 100644 index 0000000..aca04fc --- /dev/null +++ b/docs/verification-async-tps.md @@ -0,0 +1,26 @@ +# 비동기 전용 및 공통 TPS 검증 + +2026-09-14, 최신 main `1f8d8df137db2c57e0fea99b730bf4976c8fbfe0` 기준 변경이다. + +- 오프라인: 178 passed, 14 subtests passed. 실제 API 12개는 별도 opt-in이다. +- ruff 전체, mypy 25개 소스 파일, compileall 통과. +- 기존 공개 메서드 인자·기본값과 APIHub endpoint 470개/첨부 77개 metadata를 유지했다. +- 생성기로 만든 470개 endpoint를 MockTransport로 전부 await 호출하고 송신별 토큰을 확인했다. +- 독립 리뷰 A: 17개 별도 회귀 검증 및 전체 오프라인 재실행 통과, 최종 승인. + 예보 파싱 오류, APIHub resultCode 오류, 사용자 지정 인증 이름 override의 키 보호를 보강했다. +- 독립 리뷰 B: 도메인 모의 요청 20개, 생성 endpoint 470개, 관련 테스트 95개, + 문서 예제 56개 실행 통과, 최종 승인. 문서의 누락 await·세션 생성·종료와 필수 인자를 수정했다. + +두 최종 리뷰 이후 tests/test_live_services.py의 실제 API 테스트를 실행했다. +결과는 **11 passed, 1 skipped**다. 유일한 skip은 정상 데이터 응답 성공이 아니다. + +| API | 실제 결과 | 판단 | +|---|---|---| +| AsosHourlyInfoService/getWthrDataList (종관기상관측 시간자료) | HTTP 403 | 서비스키 권한 거부로 skip, 사용자에게 보고 | +| 나머지 11개 live 테스트 | 통과 | 해당 테스트의 응답·타입 계약 확인 | + +이번 실행에는 NO_DATA로 인한 skip이 없었다. 403을 허용하는 사용자 지시가 아직 없으므로 +이 PR은 **머지 보류**한다. 기존에 승인된 KHOA·datagokr·안산세계음식점·산불통계 예외를 +이 서비스의 권한 오류로 확대하지 않았다. API 전체 470개의 실서버 성공을 주장하지 않는다. + +변경 계약과 호출 예제는 [비동기 API와 공통 TPS](async-tps.md)를 참고한다. diff --git a/examples/streamlit_debug_ui.py b/examples/streamlit_debug_ui.py index b2d1ac5..81b7728 100644 --- a/examples/streamlit_debug_ui.py +++ b/examples/streamlit_debug_ui.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass +import asyncio import json import os import sys @@ -199,9 +200,7 @@ def _api_summary_lines(selected: ApiCatalogEntry) -> tuple[str, str]: return line1, line2 line1 = f"{selected.dataset_name} — APIHub {selected.service}({selected.endpoint_path}) 호출." - line2 = _RESPONSE_KIND_DESCRIPTIONS.get( - selected.response_kind, "알 수 없는 형식의 응답입니다." - ) + line2 = _RESPONSE_KIND_DESCRIPTIONS.get(selected.response_kind, "알 수 없는 형식의 응답입니다.") return line1, line2 @@ -233,14 +232,16 @@ def _raw_response_tab(selected: ApiCatalogEntry, api_key: str, *, timeout: float st.error("필수 파라미터를 입력하세요: " + ", ".join(missing)) return - run = _run_selected_api(selected, api_key, params, request_options, timeout=timeout) + run = asyncio.run( + _run_selected_api(selected, api_key, params, request_options, timeout=timeout) + ) _store_run(selected, run) if run.error: st.error(run.error["message"]) st.json(jsonable(run.response)) -def _run_selected_api( +async def _run_selected_api( selected: ApiCatalogEntry, api_key: str, params: dict[str, Any], @@ -259,18 +260,18 @@ def _run_selected_api( try: if selected.gateway == "datagokr": - client = DataGoKrClient(api_key, timeout=timeout, retries=0) - return client.debug_fetch( - selected.service or "", - selected.operation or "", - params, - page_no=request_options.get("page_no", 1), - num_of_rows=request_options.get("num_of_rows", 10), - data_type=request_options.get("data_type", "JSON"), - ) - hub_client = ApiHubGeneratedClient(api_key, timeout=timeout, retries=0) - spec = hub_client.endpoint(selected.service or "") - return hub_client.debug_fetch_endpoint(spec, params) + async with DataGoKrClient(api_key, timeout=timeout, retries=0) as client: + return await client.debug_fetch( + selected.service or "", + selected.operation or "", + params, + page_no=request_options.get("page_no", 1), + num_of_rows=request_options.get("num_of_rows", 10), + data_type=request_options.get("data_type", "JSON"), + ) + async with ApiHubGeneratedClient(api_key, timeout=timeout, retries=0) as hub_client: + spec = hub_client.endpoint(selected.service or "") + return await hub_client.debug_fetch_endpoint(spec, params) except Exception as exc: # pragma: no cover - UI 표시 return DebugRun( function=selected.service or selected.label, diff --git a/pyproject.toml b/pyproject.toml index 4c91cb9..e9b0560 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "pytest>=8", + "pytest-asyncio>=0.24", "ruff>=0.5", "mypy>=1.10", ] @@ -63,6 +64,7 @@ strict = true warn_unused_ignores = true [tool.pytest.ini_options] +asyncio_mode = "auto" testpaths = ["tests"] pythonpath = ["src"] markers = [ diff --git a/src/kma/__init__.py b/src/kma/__init__.py index 8047afd..f27c1db 100644 --- a/src/kma/__init__.py +++ b/src/kma/__init__.py @@ -1,19 +1,19 @@ """기상청 공공 날씨 API용 Python 도구.""" from ._credentials import api_key_for_gateway, env_names_for_gateway, load_local_env +from ._ratelimit import AsyncTokenBucket from .apihub import ( ApiHubAttachment, ApiHubClient, ApiHubEndpoint, ApiHubResponse, ApiHubService, - AsyncApiHubClient, ) from .apihub_endpoints import APIHUB_ATTACHMENTS, APIHUB_ENDPOINTS, ApiHubGeneratedClient from .catalog import ApiCatalogEntry, api_catalog, apihub_endpoint_catalog -from .client import AsyncForecastService, AsyncKmaClient, ForecastService, KmaClient +from .client import ForecastService, KmaClient from .codes import label_for, parse_amount, unit_for -from .datagokr import AsyncDataGoKrClient, DataGoKrClient +from .datagokr import DataGoKrClient from .datagokr_catalog import KMA_DATA_GOKR_DATASETS, DataGoKrDatasetSpec from .debug import ( DEFAULT_ASSERTION, @@ -67,6 +67,7 @@ from .timeline import pivot_forecast_items __all__ = [ + "AsyncTokenBucket", "APIHUB_ATTACHMENTS", "APIHUB_ENDPOINTS", "ApiHubClient", @@ -78,10 +79,7 @@ "ApiCatalogEntry", "AsosDailyItem", "AsosHourlyItem", - "AsyncApiHubClient", - "AsyncDataGoKrClient", - "AsyncForecastService", - "AsyncKmaClient", + "ForecastService", "BeachForecastItem", "BeachSunTime", "BeachTideItem", diff --git a/src/kma/_http.py b/src/kma/_http.py index 75da22b..6e12067 100644 --- a/src/kma/_http.py +++ b/src/kma/_http.py @@ -3,9 +3,12 @@ from __future__ import annotations import asyncio +import inspect +import logging +import math import random -import time -from collections.abc import Mapping +import re +from collections.abc import Callable, Mapping from datetime import datetime, timezone from email.utils import parsedate_to_datetime from typing import Any, NoReturn @@ -13,6 +16,8 @@ import httpx +from ._httpx import send_after_token +from ._ratelimit import AsyncTokenBucket from .exceptions import KmaAuthError, KmaRequestError, KmaServerError from .metadata import redact_credentials_in_text @@ -75,7 +80,8 @@ def _retry_after_seconds(response: httpx.Response) -> float | None: return None value = value.strip() try: - return max(0.0, float(value)) + seconds = float(value) + return max(0.0, seconds) if math.isfinite(seconds) else None except ValueError: pass try: @@ -285,26 +291,13 @@ def raise_for_kma_network_error( ) from None -def build_client() -> httpx.Client: - """Create the default synchronous httpx client.""" - - return httpx.Client(follow_redirects=True) - - def build_async_client() -> httpx.AsyncClient: """Create the default asynchronous httpx client.""" return httpx.AsyncClient(follow_redirects=True) -def build_session(retries: int = 3) -> httpx.Client: - """Backward-compatible alias for older code that asked for a session.""" - - _ = retries - return build_client() - - -def get_with_retries( +async def get_with_retries( client: Any, url: str, *, @@ -312,59 +305,42 @@ def get_with_retries( timeout: float, retries: int, backoff_factor: float = 0.3, + rate_limiter: AsyncTokenBucket | None = None, + ensure_open: Callable[[], None] | None = None, ) -> Any: - """GET a URL with retry behavior matching the old requests adapter.""" - - attempts = max(1, retries + 1) - last_exc: httpx.HTTPError | None = None - for attempt in range(attempts): - retry_after: float | None = None - try: - response = client.get(url, params=params, timeout=timeout) - response.raise_for_status() - return response - except httpx.HTTPStatusError as exc: - if not _should_retry_status(exc) or attempt >= attempts - 1: - raise - last_exc = exc - if exc.response.status_code == 429: - retry_after = _retry_after_seconds(exc.response) - except TRANSIENT_REQUEST_ERRORS as exc: - if attempt >= attempts - 1: - raise - last_exc = exc - sleep_seconds = _backoff_with_jitter(backoff_factor, attempt) - if retry_after is not None: - sleep_seconds = max(sleep_seconds, retry_after) - time.sleep(sleep_seconds) - if last_exc is not None: # pragma: no cover - defensive fallback - raise last_exc - raise RuntimeError("HTTP request failed before it could be attempted") + """Async GET a URL with retry behavior matching the sync helper.""" + validate_async_session(client) + budget = rate_limiter if rate_limiter is not None else AsyncTokenBucket(5) -async def async_get_with_retries( - client: Any, - url: str, - *, - params: dict[str, Any] | None, - timeout: float, - retries: int, - backoff_factor: float = 0.3, -) -> Any: - """Async GET a URL with retry behavior matching the sync helper.""" + def before_send() -> None: + if ensure_open is not None: + ensure_open() + validate_async_session(client) attempts = max(1, retries + 1) last_exc: httpx.HTTPError | None = None for attempt in range(attempts): + if ensure_open is not None: + ensure_open() + await budget.acquire() + if ensure_open is not None: + ensure_open() + validate_async_session(client) retry_after: float | None = None try: - response = await client.get(url, params=params, timeout=timeout) + if isinstance(client, httpx.AsyncClient): + request = client.build_request("GET", url, params=params, timeout=timeout) + response = await send_after_token(client, request, budget, before_send=before_send) + else: + response = await client.get(url, params=params, timeout=timeout) response.raise_for_status() return response except httpx.HTTPStatusError as exc: if not _should_retry_status(exc) or attempt >= attempts - 1: raise last_exc = exc + await exc.response.aclose() if exc.response.status_code == 429: retry_after = _retry_after_seconds(exc.response) except TRANSIENT_REQUEST_ERRORS as exc: @@ -382,3 +358,35 @@ async def async_get_with_retries( def _should_retry_status(exc: httpx.HTTPStatusError) -> bool: return exc.response.status_code in RETRY_STATUS_CODES + + +def validate_async_session(session: Any) -> None: + """동기 세션과 토큰 제어를 우회하는 인증 흐름을 사전 거부합니다.""" + if session is None: + return + if not inspect.iscoroutinefunction(getattr(session, "get", None)): + raise TypeError("session.get must be async") + if isinstance(session, httpx.AsyncClient): + auth = session.auth + if auth is not None and type(auth) not in {httpx.Auth, httpx.BasicAuth}: + raise TypeError("Digest/custom Auth may send unmetered requests") + + +_CREDENTIAL_PARAMS = {"servicekey", "authkey"} + + +def register_credential_param(name: str) -> None: + _CREDENTIAL_PARAMS.add(name.lower()) + + +class _KeyLogFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + names = "|".join(re.escape(name) for name in sorted(_CREDENTIAL_PARAMS)) + record.msg = re.sub( + rf"(?i)([?&](?:{names})=)[^&\s\"']+", r"\1", record.getMessage() + ) + record.args = () + return True + + +logging.getLogger("httpx").addFilter(_KeyLogFilter()) diff --git a/src/kma/_httpx.py b/src/kma/_httpx.py new file mode 100644 index 0000000..fe1ec06 --- /dev/null +++ b/src/kma/_httpx.py @@ -0,0 +1,51 @@ +"""토큰을 얻은 HTTPX 요청의 후속 redirect도 같은 예산으로 전송한다.""" + +from __future__ import annotations + +from collections.abc import Callable + +import httpx + +from ._ratelimit import AsyncTokenBucket + + +async def send_after_token( + client: httpx.AsyncClient, + request: httpx.Request, + rate_limiter: AsyncTokenBucket | None, + *, + stream: bool = False, + follow_redirects: bool | None = None, + before_send: Callable[[], None] | None = None, +) -> httpx.Response: + """호출자가 첫 토큰을 확보한 요청을 전송하고 redirect마다 추가 과금한다. + + HTTPX가 만든 next_request를 사용해 301/302/303/307/308의 메서드·쿠키· + 인증 헤더 처리를 유지한다. 최종 streaming 응답은 호출자가 닫는다. + """ + follow = client.follow_redirects if follow_redirects is None else follow_redirects + history: list[httpx.Response] = [] + while True: + if before_send is not None: + before_send() + if history: + # next_request에서 HTTPX가 제거한 cross-origin 인증을 재부착하지 않는다. + response = await client.send(request, stream=stream, follow_redirects=False, auth=None) + else: + response = await client.send(request, stream=stream, follow_redirects=False) + response.history = list(history) + next_request = response.next_request + if not follow or next_request is None: + return response + try: + if len(history) >= client.max_redirects: + raise httpx.TooManyRedirects("Exceeded maximum allowed redirects", request=request) + # 스트리밍 중간 응답은 본문을 버려 상위 계층의 메모리 상한을 우회하지 않는다. + if not stream: + await response.aread() + finally: + await response.aclose() + history.append(response) + request = next_request + if rate_limiter is not None: + await rate_limiter.acquire() diff --git a/src/kma/_ratelimit.py b/src/kma/_ratelimit.py new file mode 100644 index 0000000..1b513e1 --- /dev/null +++ b/src/kma/_ratelimit.py @@ -0,0 +1,64 @@ +"""비동기 HTTP 호출의 TPS를 제어하는 공통 토큰 버킷 구현.""" + +from __future__ import annotations + +import asyncio +import math +import time + + +class AsyncTokenBucket: + """한 이벤트 루프에서 FIFO 순서로 토큰을 배분한다. + + ``max_rps``는 초당 충전량이며 ``capacity``는 허용할 초기/최대 burst다. + 기본 용량은 ``max(1, max_rps)``이므로 1 TPS 미만도 지원한다. + 대기 중 취소된 호출은 토큰을 소비하지 않고 다음 호출에 자리를 넘긴다. + 같은 공급자 quota를 공유하는 요청에는 같은 인스턴스를 사용한다. + """ + + def __init__(self, max_rps: float = 5.0, capacity: float | None = None) -> None: + if isinstance(max_rps, bool) or not math.isfinite(max_rps) or max_rps <= 0: + raise ValueError("max_rps must be a finite number greater than 0") + resolved_capacity = max(1.0, max_rps) if capacity is None else capacity + if ( + isinstance(resolved_capacity, bool) + or not math.isfinite(resolved_capacity) + or resolved_capacity < 1 + ): + raise ValueError("capacity must be a finite number greater than or equal to 1") + self._max_rps = float(max_rps) + self._capacity = float(resolved_capacity) + self._tokens = self._capacity + self._updated_at = time.monotonic() + self._lock = asyncio.Lock() + self._loop: asyncio.AbstractEventLoop | None = None + + @property + def max_rps(self) -> float: + """초당 토큰 충전량.""" + return self._max_rps + + @property + def capacity(self) -> float: + """최대로 저장할 토큰 수.""" + return self._capacity + + async def acquire(self) -> None: + """요청 하나에 필요한 토큰이 생길 때까지 비동기로 대기한다.""" + loop = asyncio.get_running_loop() + if self._loop is None: + self._loop = loop + elif self._loop is not loop: + raise RuntimeError("AsyncTokenBucket must be used on a single event loop") + # 선두 호출만 충전을 기다린다. asyncio.Lock의 FIFO/취소 처리를 사용해 + # 별도 timer나 Future 큐 없이 후속 호출의 진행을 보장한다. + async with self._lock: + while True: + now = time.monotonic() + elapsed = max(0.0, now - self._updated_at) + self._updated_at = now + self._tokens = min(self._capacity, self._tokens + elapsed * self._max_rps) + if self._tokens >= 1: + self._tokens -= 1 + return + await asyncio.sleep((1 - self._tokens) / self._max_rps) diff --git a/src/kma/_redact.py b/src/kma/_redact.py new file mode 100644 index 0000000..5f62902 --- /dev/null +++ b/src/kma/_redact.py @@ -0,0 +1,70 @@ +"""실제 요청 인증값을 오류와 디버그 결과의 모든 필드에서 제거합니다.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import fields, is_dataclass, replace +from enum import Enum +from typing import Any, cast +from urllib.parse import quote, quote_plus, unquote + +from pydantic import BaseModel + +from .debug import DebugRun + + +def credential_values( + params: Mapping[str, Any] | None, key_name: str = "serviceKey" +) -> tuple[str, ...]: + return tuple( + str(value) + for key, value in (params or {}).items() + if key.lower().replace("_", "") in {"servicekey", "authkey"} or key == key_name + ) + + +def redact_secret(value: Any, *secrets: str) -> Any: + """모델·불변 dataclass·tuple 형태를 보존하면서 인증값을 지웁니다.""" + variants = set() + for secret in secrets: + if secret: + variants.update({secret, unquote(secret), quote(secret, safe=""), quote_plus(secret)}) + if isinstance(value, Enum): + return value + if isinstance(value, str): + for secret in sorted(variants, key=len, reverse=True): + if secret: + value = value.replace(secret, "") + return value + if isinstance(value, BaseModel): + return value.model_copy( + update={key: redact_secret(item, *secrets) for key, item in value.__dict__.items()} + ) + if is_dataclass(value) and not isinstance(value, type): + return replace( + value, + **{ + field.name: redact_secret(getattr(value, field.name), *secrets) + for field in fields(value) + }, + ) + if isinstance(value, Mapping): + return { + redact_secret(key, *secrets): redact_secret(item, *secrets) + for key, item in value.items() + } + if isinstance(value, tuple): + return tuple(redact_secret(item, *secrets) for item in value) + if isinstance(value, list): + return [redact_secret(item, *secrets) for item in value] + return value + + +def redact_exception(exc: Exception, *secrets: str) -> None: + exc.args = redact_secret(exc.args, *secrets) + for key, value in vars(exc).items(): + setattr(exc, key, redact_secret(value, *secrets)) + + +def redact_debug(run: DebugRun, *secrets: str) -> DebugRun: + return cast(DebugRun, redact_secret(run, *secrets)) diff --git a/src/kma/apihub.py b/src/kma/apihub.py index 1e0d331..2580ef7 100644 --- a/src/kma/apihub.py +++ b/src/kma/apihub.py @@ -8,7 +8,7 @@ import json import re import time -from collections.abc import AsyncIterator, Iterable, Iterator, Mapping +from collections.abc import AsyncIterator, Iterable, Mapping from dataclasses import dataclass from functools import cached_property from typing import Any @@ -19,17 +19,18 @@ from ._credentials import APIHUB_ENV_NAMES, first_env_value, normalize_api_key from ._http import ( NO_DATA_RESULT_CODE, - async_get_with_retries, build_async_client, - build_session, get_with_retries, raise_for_kma_http_error, raise_for_kma_network_error, raise_for_kma_result_code, raise_for_kma_xml_error_body, + validate_async_session, ) +from ._ratelimit import AsyncTokenBucket +from ._redact import credential_values, redact_debug, redact_exception from .debug import DebugRun, debug_error, redact_sensitive -from .exceptions import KmaParseError +from .exceptions import KmaError, KmaParseError from .metadata import ( ResponseMetadata, is_credential_param, @@ -37,7 +38,6 @@ redact_credentials_in_text, request_params_from_url, ) -from .pagination import aiter_pages as _aiter_pages from .pagination import iter_pages as _iter_pages APIHUB_BASE_URL = "https://apihub.kma.go.kr" @@ -178,76 +178,48 @@ def __init__( retries: int = 3, base_url: str = APIHUB_BASE_URL, session: Any | None = None, - async_session: Any | None = None, + max_rps: float = 5.0, + rate_limiter: AsyncTokenBucket | None = None, ) -> None: self.auth_key = normalize_api_key(auth_key, field_name="auth_key") self.timeout = timeout self.retries = retries self.base_url = _validate_apihub_base_url(base_url) - self.session = session or build_session(retries) + self._session = session self._owns_session = session is None - self._async_session = async_session - self._owns_async_session = async_session is None - - @classmethod - def from_env(cls, name: str = "KMA_APIHUB_AUTH_KEY", **kwargs: Any) -> ApiHubClient: - auth_key = first_env_value((name, *APIHUB_ENV_NAMES)) - return cls(auth_key, **kwargs) - - @classmethod - def aio(cls, auth_key: str, **kwargs: Any) -> AsyncApiHubClient: - """Create an async facade for the APIHub gateway.""" - - return AsyncApiHubClient(auth_key, **kwargs) - - @classmethod - def aio_from_env(cls, name: str = "KMA_APIHUB_AUTH_KEY", **kwargs: Any) -> AsyncApiHubClient: - """Create an async facade from environment credentials.""" + validate_async_session(session) + self.rate_limiter = rate_limiter if rate_limiter is not None else AsyncTokenBucket(max_rps) + self.closed = False - return AsyncApiHubClient.from_env(name=name, **kwargs) + def _ensure_open(self) -> None: + if self.closed: + raise RuntimeError("client is closed") - def close(self) -> None: - close = getattr(self.session, "close", None) - if self._owns_session and close is not None: - close() + @property + def session(self) -> Any: + return self._get_session() async def aclose(self) -> None: - if self._async_session is None or not self._owns_async_session: - return - aclose = getattr(self._async_session, "aclose", None) - close = getattr(self._async_session, "close", None) - if aclose is not None: - await aclose() - elif close is not None: - close() - - def __enter__(self) -> ApiHubClient: - return self + if not self.closed: + self.closed = True + if self._owns_session and self._session is not None: + close = getattr(self._session, "aclose", None) + if callable(close): + await close() - def __exit__(self, *_exc: object) -> None: - self.close() + @classmethod + def from_env(cls, name: str = "KMA_APIHUB_AUTH_KEY", **kwargs: Any) -> ApiHubClient: + auth_key = first_env_value((name, *APIHUB_ENV_NAMES)) + return cls(auth_key, **kwargs) async def __aenter__(self) -> ApiHubClient: + self._ensure_open() return self async def __aexit__(self, *_exc: object) -> None: await self.aclose() - def request_path( - self, - path: str, - params: Mapping[str, Any] | None = None, - ) -> ApiHubResponse: - """`/api/...` 아래 APIHub path를 호출하고 `authKey`를 추가합니다.""" - - clean_path = _normalize_apihub_path(path) - request_params: dict[str, Any] = {} - if params: - request_params.update(params) - request_params["authKey"] = self.auth_key - return self._get(clean_path, request_params) - - async def arequest_path( + async def request_path( self, path: str, params: Mapping[str, Any] | None = None, @@ -259,42 +231,9 @@ async def arequest_path( if params: request_params.update(params) request_params["authKey"] = self.auth_key - return await self._aget(clean_path, request_params) + return await self._get(clean_path, request_params) - def request_query_parts( - self, - path: str, - query_parts: Iterable[tuple[str, str]], - params: Mapping[str, Any] | None = None, - ) -> ApiHubResponse: - """이름 없는 query string 조각이 있는 APIHub endpoint를 호출합니다. - - 일부 legacy 그래픽 endpoint는 ``...?202305031000&0&stn-list&authKey=...`` - 같은 URL을 사용합니다. 일반적인 key-value query parameter가 아니므로 - `requests.get(..., params=...)`로 재현할 수 없습니다. `query_parts`는 각 - 항목을 `("bare", name)` 또는 `("named", name)`으로 저장하고, 이 메서드는 - query string을 직접 직렬화합니다. - """ - - clean_path = _normalize_apihub_path(path) - values = dict(params or {}) - fragments: list[str] = [] - for kind, name in query_parts: - if name == "authKey": - continue - if name not in values: - continue - value = values[name] - if value is None: - continue - if kind == "bare": - fragments.append(_quote_query_value(value)) - else: - fragments.append(f"{quote_plus(name)}={_quote_query_value(value)}") - fragments.append(f"authKey={_quote_query_value(self.auth_key)}") - return self._get_raw(f"{clean_path}?{'&'.join(fragments)}") - - async def arequest_query_parts( + async def request_query_parts( self, path: str, query_parts: Iterable[tuple[str, str]], @@ -318,33 +257,9 @@ async def arequest_query_parts( else: fragments.append(f"{quote_plus(name)}={_quote_query_value(value)}") fragments.append(f"authKey={_quote_query_value(self.auth_key)}") - return await self._aget_raw(f"{clean_path}?{'&'.join(fragments)}") - - def open_api( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> ApiHubResponse: - """`/api/typ02/openApi/{service}/{operation}` endpoint를 호출합니다.""" - - request_params: dict[str, Any] = { - "pageNo": page_no, - "numOfRows": num_of_rows, - "dataType": data_type, - } - if params: - request_params.update(params) - endpoint = f"/api/typ02/openApi/{service.strip('/')}/{operation.strip('/')}" - response = self.request_path(endpoint, request_params) - _check_apihub_result_code(response, endpoint=endpoint) - return response + return await self._get_raw(f"{clean_path}?{'&'.join(fragments)}") - async def aopen_api( + async def open_api( self, service: str, operation: str, @@ -364,57 +279,43 @@ async def aopen_api( if params: request_params.update(params) endpoint = f"/api/typ02/openApi/{service.strip('/')}/{operation.strip('/')}" - response = await self.arequest_path(endpoint, request_params) - _check_apihub_result_code(response, endpoint=endpoint) + response = await self.request_path(endpoint, request_params) + try: + _check_apihub_result_code(response, endpoint=endpoint) + except KmaError as exc: + redact_exception(exc, self.auth_key, *credential_values(params)) + raise exc from None return response - def discover_services( - self, - category_ids: tuple[int, ...] = APIHUB_CATEGORY_IDS, - ) -> list[ApiHubService]: - """공식 category id 목록에 대한 APIHub service 목록을 가져옵니다.""" - - services: list[ApiHubService] = [] - for category_id in category_ids: - response = self._portal_get("/apiList.do", {"seqApi": category_id}) - services.extend(parse_apihub_services(response.text, category_id)) - return services - - async def adiscover_services( + async def discover_services( self, category_ids: tuple[int, ...] = APIHUB_CATEGORY_IDS, ) -> list[ApiHubService]: """Asynchronously fetch APIHub service metadata.""" + try: + services: list[ApiHubService] = [] + for category_id in category_ids: + response = await self._portal_get("/apiList.do", {"seqApi": category_id}) + services.extend(parse_apihub_services(response.text, category_id)) + return services + except KmaError as exc: + redact_exception(exc, self.auth_key, *credential_values(None)) + raise exc from None - services: list[ApiHubService] = [] - for category_id in category_ids: - response = await self._aportal_get("/apiList.do", {"seqApi": category_id}) - services.extend(parse_apihub_services(response.text, category_id)) - return services - - def discover_endpoints(self, category_id: int, service_id: int) -> list[ApiHubEndpoint]: - """하나의 APIHub service page에서 endpoint 예제를 가져옵니다.""" - - response = self._portal_get( - "/apiList.do", - {"seqApi": category_id, "seqApiSub": service_id}, - ) - return extract_apihub_endpoints(response.text) - - async def adiscover_endpoints( + async def discover_endpoints( self, category_id: int, service_id: int, ) -> list[ApiHubEndpoint]: """Asynchronously fetch endpoint samples for one APIHub service page.""" - response = await self._aportal_get( + response = await self._portal_get( "/apiList.do", {"seqApi": category_id, "seqApiSub": service_id}, ) return extract_apihub_endpoints(response.text) - def debug_fetch_endpoint( + async def debug_fetch_endpoint( self, spec: ApiHubEndpointSpec, params: Mapping[str, Any] | None = None, @@ -434,73 +335,86 @@ def debug_fetch_endpoint( 나머지는 `request_path`로 호출을 위임합니다(``ApiHubGeneratedClient .call_endpoint``와 같은 판정 규칙). """ + try: + request_params: dict[str, Any] = {} + if use_sample: + request_params.update(spec.sample_params) + if params: + request_params.update(params) + + input_data = redact_sensitive( + { + "endpoint": spec.name, + "params": request_params, + "use_sample": use_sample, + } + ) + trace = [ + f"APIHub {spec.name} ({spec.path}) 호출 준비", + f"response_kind={spec.response_kind}", + ] + request_info = redact_sensitive( + { + "method": "GET", + "url": f"{self.base_url}{spec.path}", + "query": request_params, + } + ) - request_params: dict[str, Any] = {} - if use_sample: - request_params.update(spec.sample_params) - if params: - request_params.update(params) - - input_data = redact_sensitive( - { - "endpoint": spec.name, - "params": request_params, - "use_sample": use_sample, - } - ) - trace = [ - f"APIHub {spec.name} ({spec.path}) 호출 준비", - f"response_kind={spec.response_kind}", - ] - request_info = redact_sensitive( - { - "method": "GET", - "url": f"{self.base_url}{spec.path}", - "query": request_params, - } - ) + started_at = time.monotonic() + try: + if any(kind == "bare" for kind, _name in spec.query_parts): + response = await self.request_query_parts( + spec.path, spec.query_parts, request_params + ) + else: + response = await self.request_path(spec.path, request_params) + except Exception as exc: + elapsed_ms = (time.monotonic() - started_at) * 1000 + trace.append(f"요청 실패: {exc.__class__.__name__} ({elapsed_ms:.0f}ms)") + return redact_debug( + DebugRun( + function=spec.name, + input=input_data, + request=request_info, + response={}, + parsed=None, + processed=None, + trace=trace, + error=debug_error(exc), + ), + self.auth_key, + *credential_values(params), + ) - started_at = time.monotonic() - try: - if any(kind == "bare" for kind, _name in spec.query_parts): - response = self.request_query_parts(spec.path, spec.query_parts, request_params) - else: - response = self.request_path(spec.path, request_params) - except Exception as exc: elapsed_ms = (time.monotonic() - started_at) * 1000 - trace.append(f"요청 실패: {exc.__class__.__name__} ({elapsed_ms:.0f}ms)") - return DebugRun( - function=spec.name, - input=input_data, - request=request_info, - response={}, - parsed=None, - processed=None, - trace=trace, - error=debug_error(exc), + trace.append( + f"응답 수신: HTTP {response.status_code}, {len(response.content)} bytes " + f"({elapsed_ms:.0f}ms)" ) + parsed, processed = _debug_parse_apihub_response(response, spec.response_kind) + return redact_debug( + DebugRun( + function=spec.name, + input=input_data, + request=request_info, + response={ + "status_code": response.status_code, + "content_type": response.content_type, + "body": parsed, + }, + parsed=parsed, + processed=processed, + trace=trace, + ), + self.auth_key, + *credential_values(params), + ) + except KmaError as exc: + redact_exception(exc, self.auth_key, *credential_values(params)) + raise exc from None - elapsed_ms = (time.monotonic() - started_at) * 1000 - trace.append( - f"응답 수신: HTTP {response.status_code}, {len(response.content)} bytes " - f"({elapsed_ms:.0f}ms)" - ) - parsed, processed = _debug_parse_apihub_response(response, spec.response_kind) - return DebugRun( - function=spec.name, - input=input_data, - request=request_info, - response={ - "status_code": response.status_code, - "content_type": response.content_type, - "body": parsed, - }, - parsed=parsed, - processed=processed, - trace=trace, - ) - - def iter_pages( + async def iter_pages( self, service: str, operation: str, @@ -511,275 +425,96 @@ def iter_pages( num_of_rows: int = 10, max_pages: int = 100, max_items: int | None = None, - ) -> Iterator[Mapping[str, Any]]: - """명시적 안전장치와 함께 APIHub `open_api` 페이지네이션 응답 body를 순회합니다.""" + ) -> AsyncIterator[Mapping[str, Any]]: + """Asynchronously iterate paginated APIHub `open_api` response bodies.""" + try: + endpoint = f"/api/typ02/openApi/{service.strip('/')}/{operation.strip('/')}" - endpoint = f"/api/typ02/openApi/{service.strip('/')}/{operation.strip('/')}" - return _iter_pages( - lambda page_no: _apihub_open_api_body( - self.open_api( + async def _fetch_page(page_no: int) -> Mapping[str, Any]: + response = await self.open_api( service, operation, params, data_type=data_type, page_no=page_no, num_of_rows=num_of_rows, - ), - endpoint=endpoint, - ), - start_page=start_page, - max_pages=max_pages, - max_items=max_items, - ) - - async def aiter_pages( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - start_page: int = 1, - num_of_rows: int = 10, - max_pages: int = 100, - max_items: int | None = None, - ) -> AsyncIterator[Mapping[str, Any]]: - """Asynchronously iterate paginated APIHub `open_api` response bodies.""" - - endpoint = f"/api/typ02/openApi/{service.strip('/')}/{operation.strip('/')}" - - async def _fetch_page(page_no: int) -> Mapping[str, Any]: - response = await self.aopen_api( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return _apihub_open_api_body(response, endpoint=endpoint) - - async for body in _aiter_pages( - _fetch_page, - start_page=start_page, - max_pages=max_pages, - max_items=max_items, - ): - yield body - - def _portal_get(self, path: str, params: Mapping[str, Any]) -> ApiHubResponse: - return self._get(path, params) - - async def _aportal_get(self, path: str, params: Mapping[str, Any]) -> ApiHubResponse: - return await self._aget(path, params) - - def _get(self, path: str, params: Mapping[str, Any]) -> ApiHubResponse: - return self._get_url( - f"{self.base_url}/{path.lstrip('/')}", - params=dict(params), - ) - - async def _aget(self, path: str, params: Mapping[str, Any]) -> ApiHubResponse: - return await self._aget_url( + ) + return _apihub_open_api_body(response, endpoint=endpoint) + + async for body in _iter_pages( + _fetch_page, + start_page=start_page, + max_pages=max_pages, + max_items=max_items, + ): + yield body + except KmaError as exc: + redact_exception(exc, self.auth_key, *credential_values(params)) + raise exc from None + + async def _portal_get(self, path: str, params: Mapping[str, Any]) -> ApiHubResponse: + return await self._get(path, params) + + async def _get(self, path: str, params: Mapping[str, Any]) -> ApiHubResponse: + return await self._get_url( f"{self.base_url}/{path.lstrip('/')}", params=dict(params), ) - def _get_raw(self, path_with_query: str) -> ApiHubResponse: - return self._get_url(f"{self.base_url}/{path_with_query.lstrip('/')}", params=None) + async def _get_raw(self, path_with_query: str) -> ApiHubResponse: + return await self._get_url(f"{self.base_url}/{path_with_query.lstrip('/')}", params=None) - async def _aget_raw(self, path_with_query: str) -> ApiHubResponse: - return await self._aget_url(f"{self.base_url}/{path_with_query.lstrip('/')}", params=None) - - def _get_url(self, url: str, params: Mapping[str, Any] | None) -> ApiHubResponse: - endpoint = urlsplit(url).path - metadata = make_response_metadata( - provider="apihub", - service_name="APIHub", - endpoint=endpoint, - request_params=params if params is not None else request_params_from_url(url), - ) + async def _get_url(self, url: str, params: Mapping[str, Any] | None) -> ApiHubResponse: try: - response = get_with_retries( - self.session, - url, - params=dict(params) if params is not None else None, - timeout=self.timeout, - retries=self.retries, - ) - except httpx.HTTPStatusError as exc: - raise_for_kma_http_error( - exc, + endpoint = urlsplit(url).path + metadata = make_response_metadata( provider="apihub", + service_name="APIHub", endpoint=endpoint, - label="APIHub", - detail=_response_error_message(exc.response), + request_params=params if params is not None else request_params_from_url(url), ) - except httpx.RequestError: - raise_for_kma_network_error( - provider="apihub", - endpoint=endpoint, - label="APIHub", - ) - - content_type = response.headers.get("Content-Type", "") - return ApiHubResponse( - url=redact_url_credentials(str(response.url)), - status_code=response.status_code, - content_type=content_type, - content=response.content, - metadata=metadata, - ) + try: + response = await get_with_retries( + self._get_session(), + url, + params=dict(params) if params is not None else None, + timeout=self.timeout, + retries=self.retries, + rate_limiter=self.rate_limiter, + ensure_open=self._ensure_open, + ) + except httpx.HTTPStatusError as exc: + raise_for_kma_http_error( + exc, + provider="apihub", + endpoint=endpoint, + label="APIHub", + detail=_response_error_message(exc.response), + ) + except httpx.RequestError: + raise_for_kma_network_error( + provider="apihub", + endpoint=endpoint, + label="APIHub", + ) - async def _aget_url(self, url: str, params: Mapping[str, Any] | None) -> ApiHubResponse: - endpoint = urlsplit(url).path - metadata = make_response_metadata( - provider="apihub", - service_name="APIHub", - endpoint=endpoint, - request_params=params if params is not None else request_params_from_url(url), - ) - try: - response = await async_get_with_retries( - self._get_async_session(), - url, - params=dict(params) if params is not None else None, - timeout=self.timeout, - retries=self.retries, + content_type = response.headers.get("Content-Type", "") + return ApiHubResponse( + url=redact_url_credentials(str(response.url)), + status_code=response.status_code, + content_type=content_type, + content=response.content, + metadata=metadata, ) - except httpx.HTTPStatusError as exc: - raise_for_kma_http_error( - exc, - provider="apihub", - endpoint=endpoint, - label="APIHub", - detail=_response_error_message(exc.response), - ) - except httpx.RequestError: - raise_for_kma_network_error( - provider="apihub", - endpoint=endpoint, - label="APIHub", - ) - - content_type = response.headers.get("Content-Type", "") - return ApiHubResponse( - url=redact_url_credentials(str(response.url)), - status_code=response.status_code, - content_type=content_type, - content=response.content, - metadata=metadata, - ) - - def _get_async_session(self) -> Any: - if self._async_session is None: - self._async_session = build_async_client() - return self._async_session - - -class AsyncApiHubClient: - """Asynchronous facade for the APIHub gateway. - - Mirrors :class:`AsyncKmaClient`: ``ApiHubClient.aio()`` returns one of - these, exposing the same method names as the synchronous client but as - coroutines (delegating to the ``a``-prefixed methods underneath). - """ - - def __init__(self, auth_key: str, **kwargs: Any) -> None: - self._client = ApiHubClient(auth_key, **kwargs) - self.auth_key = self._client.auth_key - self.config = { - "base_url": self._client.base_url, - "timeout": self._client.timeout, - "retries": self._client.retries, - } - self.closed = False - - @classmethod - def from_env(cls, name: str = "KMA_APIHUB_AUTH_KEY", **kwargs: Any) -> AsyncApiHubClient: - auth_key = first_env_value((name, *APIHUB_ENV_NAMES)) - return cls(auth_key, **kwargs) - - async def __aenter__(self) -> AsyncApiHubClient: - return self - - async def __aexit__(self, *_exc: object) -> None: - await self.aclose() - - async def aclose(self) -> None: - await self._client.aclose() - self._client.close() - self.closed = True - - async def request_path( - self, - path: str, - params: Mapping[str, Any] | None = None, - ) -> ApiHubResponse: - return await self._client.arequest_path(path, params) - - async def request_query_parts( - self, - path: str, - query_parts: Iterable[tuple[str, str]], - params: Mapping[str, Any] | None = None, - ) -> ApiHubResponse: - return await self._client.arequest_query_parts(path, query_parts, params) - - async def open_api( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> ApiHubResponse: - return await self._client.aopen_api( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - - async def discover_services( - self, - category_ids: tuple[int, ...] = APIHUB_CATEGORY_IDS, - ) -> list[ApiHubService]: - return await self._client.adiscover_services(category_ids) - - async def discover_endpoints( - self, - category_id: int, - service_id: int, - ) -> list[ApiHubEndpoint]: - return await self._client.adiscover_endpoints(category_id, service_id) - - def iter_pages( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - start_page: int = 1, - num_of_rows: int = 10, - max_pages: int = 100, - max_items: int | None = None, - ) -> AsyncIterator[Mapping[str, Any]]: - return self._client.aiter_pages( - service, - operation, - params, - data_type=data_type, - start_page=start_page, - num_of_rows=num_of_rows, - max_pages=max_pages, - max_items=max_items, - ) + except KmaError as exc: + redact_exception(exc, self.auth_key, *credential_values(params)) + raise exc from None + + def _get_session(self) -> Any: + self._ensure_open() + if self._session is None: + self._session = build_async_client() + return self._session def parse_apihub_services(html_text: str, category_id: int) -> list[ApiHubService]: diff --git a/src/kma/apihub_endpoints.py b/src/kma/apihub_endpoints.py index f5b1446..2faf90e 100644 --- a/src/kma/apihub_endpoints.py +++ b/src/kma/apihub_endpoints.py @@ -7394,24 +7394,8 @@ def endpoint(self, name: str) -> ApiHubEndpointSpec: def sample_params(self, name: str) -> Mapping[str, str]: return self.endpoint(name).sample_params - def call_endpoint( - self, - name: str, - params: Mapping[str, Any] | None = None, - *, - use_sample: bool = False, - ) -> ApiHubResponse: - spec = self.endpoint(name) - request_params: dict[str, Any] = {} - if use_sample: - request_params.update(spec.sample_params) - if params: - request_params.update(params) - if any(kind == "bare" for kind, _name in spec.query_parts): - return self.request_query_parts(spec.path, spec.query_parts, request_params) - return self.request_path(spec.path, request_params) - async def acall_endpoint( + async def call_endpoint( self, name: str, params: Mapping[str, Any] | None = None, @@ -7425,22 +7409,11 @@ async def acall_endpoint( if params: request_params.update(params) if any(kind == "bare" for kind, _name in spec.query_parts): - return await self.arequest_query_parts(spec.path, spec.query_parts, request_params) - return await self.arequest_path(spec.path, request_params) + return await self.request_query_parts(spec.path, spec.query_parts, request_params) + return await self.request_path(spec.path, request_params) - def text_endpoint( - self, - name: str, - params: Mapping[str, Any] | None = None, - *, - use_sample: bool = False, - delimiter: str | None = None, - ) -> ApiHubTextTable: - return self.call_endpoint(name, params, use_sample=use_sample).text_table( - delimiter=delimiter - ) - async def atext_endpoint( + async def text_endpoint( self, name: str, params: Mapping[str, Any] | None = None, @@ -7448,29 +7421,21 @@ async def atext_endpoint( use_sample: bool = False, delimiter: str | None = None, ) -> ApiHubTextTable: - response = await self.acall_endpoint(name, params, use_sample=use_sample) + response = await self.call_endpoint(name, params, use_sample=use_sample) return response.text_table(delimiter=delimiter) - def image_endpoint( - self, - name: str, - params: Mapping[str, Any] | None = None, - *, - use_sample: bool = False, - ) -> ApiHubImage: - return self.call_endpoint(name, params, use_sample=use_sample).image() - async def aimage_endpoint( + async def image_endpoint( self, name: str, params: Mapping[str, Any] | None = None, *, use_sample: bool = False, ) -> ApiHubImage: - response = await self.acall_endpoint(name, params, use_sample=use_sample) + response = await self.call_endpoint(name, params, use_sample=use_sample) return response.image() - def kma_sfctm2( + async def kma_sfctm2( self, *, use_sample: bool = False, @@ -7480,9 +7445,9 @@ def kma_sfctm2( Path: /api/typ01/url/kma_sfctm2.php 파라미터: tm, stn, help""" - return self.call_endpoint('kma_sfctm2', params, use_sample=use_sample) + return (await self.call_endpoint('kma_sfctm2', params, use_sample=use_sample)) - def kma_sfctm3( + async def kma_sfctm3( self, *, use_sample: bool = False, @@ -7493,9 +7458,9 @@ def kma_sfctm3( Path: /api/typ01/url/kma_sfctm3.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('kma_sfctm3', params, use_sample=use_sample) + return (await self.call_endpoint('kma_sfctm3', params, use_sample=use_sample)) - def kma_sfcdd( + async def kma_sfcdd( self, *, use_sample: bool = False, @@ -7505,9 +7470,9 @@ def kma_sfcdd( Path: /api/typ01/url/kma_sfcdd.php 파라미터: tm, stn, help""" - return self.call_endpoint('kma_sfcdd', params, use_sample=use_sample) + return (await self.call_endpoint('kma_sfcdd', params, use_sample=use_sample)) - def kma_sfcdd3( + async def kma_sfcdd3( self, *, use_sample: bool = False, @@ -7518,9 +7483,9 @@ def kma_sfcdd3( Path: /api/typ01/url/kma_sfcdd3.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('kma_sfcdd3', params, use_sample=use_sample) + return (await self.call_endpoint('kma_sfcdd3', params, use_sample=use_sample)) - def kma_sfctm5( + async def kma_sfctm5( self, *, use_sample: bool = False, @@ -7531,9 +7496,9 @@ def kma_sfctm5( Path: /api/typ01/url/kma_sfctm5.php 파라미터: tm2, obs, stn, disp, help""" - return self.call_endpoint('kma_sfctm5', params, use_sample=use_sample) + return (await self.call_endpoint('kma_sfctm5', params, use_sample=use_sample)) - def sfc_norm1( + async def sfc_norm1( self, *, use_sample: bool = False, @@ -7544,9 +7509,9 @@ def sfc_norm1( Path: /api/typ01/url/sfc_norm1.php 파라미터: norm, tmst, stn, MM1, DD1, MM2, DD2""" - return self.call_endpoint('sfc_norm1', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_norm1', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_year_sumry( + async def sfc_yearly_info_service_get_year_sumry( self, *, use_sample: bool = False, @@ -7556,9 +7521,9 @@ def sfc_yearly_info_service_get_year_sumry( Path: /api/typ02/openApi/SfcYearlyInfoService/getYearSumry 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_get_year_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_year_sumry', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_year_sumry2( + async def sfc_yearly_info_service_get_year_sumry2( self, *, use_sample: bool = False, @@ -7570,9 +7535,9 @@ def sfc_yearly_info_service_get_year_sumry2( /api/typ02/openApi/SfcYearlyInfoService/getYearSumry2 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_get_year_sumry2', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_year_sumry2', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_avg_ta_anamaly( + async def sfc_yearly_info_service_get_avg_ta_anamaly( self, *, use_sample: bool = False, @@ -7584,9 +7549,9 @@ def sfc_yearly_info_service_get_avg_ta_anamaly( /api/typ02/openApi/SfcYearlyInfoService/getAvgTaAnamaly 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_get_avg_ta_anamaly', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_avg_ta_anamaly', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_rn_anamaly( + async def sfc_yearly_info_service_get_rn_anamaly( self, *, use_sample: bool = False, @@ -7596,9 +7561,9 @@ def sfc_yearly_info_service_get_rn_anamaly( Path: /api/typ02/openApi/SfcYearlyInfoService/getRnAnamaly 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_get_rn_anamaly', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_rn_anamaly', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_stn_phnmn_data( + async def sfc_yearly_info_service_get_stn_phnmn_data( self, *, use_sample: bool = False, @@ -7610,9 +7575,9 @@ def sfc_yearly_info_service_get_stn_phnmn_data( /api/typ02/openApi/SfcYearlyInfoService/getStnPhnmnData 파라미터: pageNo, numOfRows, dataType, year, station""" - return self.call_endpoint('sfc_yearly_info_service_get_stn_phnmn_data', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_stn_phnmn_data', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_stn_phnmn_data2( + async def sfc_yearly_info_service_get_stn_phnmn_data2( self, *, use_sample: bool = False, @@ -7624,9 +7589,9 @@ def sfc_yearly_info_service_get_stn_phnmn_data2( /api/typ02/openApi/SfcYearlyInfoService/getStnPhnmnData2 파라미터: pageNo, numOfRows, dataType, year, station""" - return self.call_endpoint('sfc_yearly_info_service_get_stn_phnmn_data2', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_stn_phnmn_data2', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_stn_phnmn_data3( + async def sfc_yearly_info_service_get_stn_phnmn_data3( self, *, use_sample: bool = False, @@ -7638,9 +7603,9 @@ def sfc_yearly_info_service_get_stn_phnmn_data3( /api/typ02/openApi/SfcYearlyInfoService/getStnPhnmnData3 파라미터: pageNo, numOfRows, dataType, year, station""" - return self.call_endpoint('sfc_yearly_info_service_get_stn_phnmn_data3', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_stn_phnmn_data3', params, use_sample=use_sample)) - def sfc_mtly_info_service_get_note( + async def sfc_mtly_info_service_get_note( self, *, use_sample: bool = False, @@ -7651,9 +7616,9 @@ def sfc_mtly_info_service_get_note( Path: /api/typ02/openApi/SfcMtlyInfoService/getNote 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sfc_mtly_info_service_get_note', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_get_note', params, use_sample=use_sample)) - def sfc_mtly_info_service_get_sfc_stn_lst_tbl( + async def sfc_mtly_info_service_get_sfc_stn_lst_tbl( self, *, use_sample: bool = False, @@ -7665,9 +7630,9 @@ def sfc_mtly_info_service_get_sfc_stn_lst_tbl( /api/typ02/openApi/SfcMtlyInfoService/getSfcStnLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sfc_mtly_info_service_get_sfc_stn_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_get_sfc_stn_lst_tbl', params, use_sample=use_sample)) - def sfc_mtly_info_service_get_mm_sumry( + async def sfc_mtly_info_service_get_mm_sumry( self, *, use_sample: bool = False, @@ -7678,9 +7643,9 @@ def sfc_mtly_info_service_get_mm_sumry( Path: /api/typ02/openApi/SfcMtlyInfoService/getMmSumry 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sfc_mtly_info_service_get_mm_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_get_mm_sumry', params, use_sample=use_sample)) - def sfc_mtly_info_service_get_mm_sumry2( + async def sfc_mtly_info_service_get_mm_sumry2( self, *, use_sample: bool = False, @@ -7690,9 +7655,9 @@ def sfc_mtly_info_service_get_mm_sumry2( Path: /api/typ02/openApi/SfcMtlyInfoService/getMmSumry2 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sfc_mtly_info_service_get_mm_sumry2', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_get_mm_sumry2', params, use_sample=use_sample)) - def sfc_mtly_info_service_get_daily_wthr_data( + async def sfc_mtly_info_service_get_daily_wthr_data( self, *, use_sample: bool = False, @@ -7704,9 +7669,9 @@ def sfc_mtly_info_service_get_daily_wthr_data( /api/typ02/openApi/SfcMtlyInfoService/getDailyWthrData 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('sfc_mtly_info_service_get_daily_wthr_data', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_get_daily_wthr_data', params, use_sample=use_sample)) - def alw_sfc_sfc_ww_pnt( + async def alw_sfc_sfc_ww_pnt( self, *, use_sample: bool = False, @@ -7717,9 +7682,9 @@ def alw_sfc_sfc_ww_pnt( Path: /api/typ03/php/alw/sfc/sfc_ww_pnt.php 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('alw_sfc_sfc_ww_pnt', params, use_sample=use_sample) + return (await self.call_endpoint('alw_sfc_sfc_ww_pnt', params, use_sample=use_sample)) - def aws2_min( + async def aws2_min( self, *, use_sample: bool = False, @@ -7730,9 +7695,9 @@ def aws2_min( Path: /api/typ01/cgi-bin/url/nph-aws2_min 파라미터: tm2, stn, disp, help""" - return self.call_endpoint('aws2_min', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min', params, use_sample=use_sample)) - def aws2_min_lst( + async def aws2_min_lst( self, *, use_sample: bool = False, @@ -7743,9 +7708,9 @@ def aws2_min_lst( Path: /api/typ01/cgi-bin/url/nph-aws2_min_lst 파라미터: tm2, stn, disp, help""" - return self.call_endpoint('aws2_min_lst', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_lst', params, use_sample=use_sample)) - def aws2_min_cloud( + async def aws2_min_cloud( self, *, use_sample: bool = False, @@ -7756,9 +7721,9 @@ def aws2_min_cloud( Path: /api/typ01/cgi-bin/url/nph-aws2_min_cloud 파라미터: tm2, stn, disp, help""" - return self.call_endpoint('aws2_min_cloud', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_cloud', params, use_sample=use_sample)) - def aws2_min_ca2( + async def aws2_min_ca2( self, *, use_sample: bool = False, @@ -7768,9 +7733,9 @@ def aws2_min_ca2( Path: /api/typ01/cgi-bin/url/nph-aws2_min_ca2 파라미터: tm2, itv, range, stn, disp, help""" - return self.call_endpoint('aws2_min_ca2', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_ca2', params, use_sample=use_sample)) - def aws2_min_ca3( + async def aws2_min_ca3( self, *, use_sample: bool = False, @@ -7781,9 +7746,9 @@ def aws2_min_ca3( Path: /api/typ01/cgi-bin/url/nph- aws2_min_ca3 파라미터: tm2, itv, range, stn, disp, help""" - return self.call_endpoint('aws2_min_ca3', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_ca3', params, use_sample=use_sample)) - def aws2_min_vis( + async def aws2_min_vis( self, *, use_sample: bool = False, @@ -7794,9 +7759,9 @@ def aws2_min_vis( Path: /api/typ01/cgi-bin/url/nph-aws2_min_vis 파라미터: tm2, stn, disp, help""" - return self.call_endpoint('aws2_min_vis', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_vis', params, use_sample=use_sample)) - def aws2_min_vis3( + async def aws2_min_vis3( self, *, use_sample: bool = False, @@ -7807,9 +7772,9 @@ def aws2_min_vis3( Path: /api/typ01/cgi-bin/url/nph- aws2_min_vis3 파라미터: tm2, itv, range, stn, disp, help""" - return self.call_endpoint('aws2_min_vis3', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_vis3', params, use_sample=use_sample)) - def aws2_min_ww1( + async def aws2_min_ww1( self, *, use_sample: bool = False, @@ -7820,9 +7785,9 @@ def aws2_min_ww1( Path: /api/typ01/cgi-bin/url/nph-aws2_min_ww1 파라미터: tm2, itv, range, stn, help""" - return self.call_endpoint('aws2_min_ww1', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_ww1', params, use_sample=use_sample)) - def aws2_min_ww2( + async def aws2_min_ww2( self, *, use_sample: bool = False, @@ -7833,9 +7798,9 @@ def aws2_min_ww2( Path: /api/typ01/cgi-bin/url/nph-aws2_min_ww2 파라미터: tm2, itv, range, stn, disp, help""" - return self.call_endpoint('aws2_min_ww2', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_min_ww2', params, use_sample=use_sample)) - def aws3_min_mob( + async def aws3_min_mob( self, *, use_sample: bool = False, @@ -7846,9 +7811,9 @@ def aws3_min_mob( Path: /api/typ01/cgi-bin/url/nph-aws3_min_mob 파라미터: tm1, tm2, stn, disp, help""" - return self.call_endpoint('aws3_min_mob', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_min_mob', params, use_sample=use_sample)) - def awsh( + async def awsh( self, *, use_sample: bool = False, @@ -7858,9 +7823,9 @@ def awsh( Path: /api/typ01/url/awsh.php 파라미터: var, tm, help""" - return self.call_endpoint('awsh', params, use_sample=use_sample) + return (await self.call_endpoint('awsh', params, use_sample=use_sample)) - def awsh_2( + async def awsh_2( self, *, use_sample: bool = False, @@ -7870,9 +7835,9 @@ def awsh_2( Path: /api/typ01/url/awsh.php 파라미터: tm, help""" - return self.call_endpoint('awsh_2', params, use_sample=use_sample) + return (await self.call_endpoint('awsh_2', params, use_sample=use_sample)) - def sfc_aws_day( + async def sfc_aws_day( self, *, use_sample: bool = False, @@ -7883,9 +7848,9 @@ def sfc_aws_day( Path: /api/typ01/url/sfc_aws_day.php 파라미터: tm2, obs, stn, disp, help""" - return self.call_endpoint('sfc_aws_day', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_aws_day', params, use_sample=use_sample)) - def aws_yearly_info_service_get_stnby_mm_sumry( + async def aws_yearly_info_service_get_stnby_mm_sumry( self, *, use_sample: bool = False, @@ -7897,9 +7862,9 @@ def aws_yearly_info_service_get_stnby_mm_sumry( /api/typ02/openApi/AwsYearlyInfoService/getStnbyMmSumry 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('aws_yearly_info_service_get_stnby_mm_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('aws_yearly_info_service_get_stnby_mm_sumry', params, use_sample=use_sample)) - def aws_yearly_info_service_get_year_sumry( + async def aws_yearly_info_service_get_year_sumry( self, *, use_sample: bool = False, @@ -7909,9 +7874,9 @@ def aws_yearly_info_service_get_year_sumry( Path: /api/typ02/openApi/AwsYearlyInfoService/getYearSumry 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('aws_yearly_info_service_get_year_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('aws_yearly_info_service_get_year_sumry', params, use_sample=use_sample)) - def aws_yearly_info_service_get_aws_stn_lst_tbl( + async def aws_yearly_info_service_get_aws_stn_lst_tbl( self, *, use_sample: bool = False, @@ -7923,9 +7888,9 @@ def aws_yearly_info_service_get_aws_stn_lst_tbl( /api/typ02/openApi/AwsYearlyInfoService/getAwsStnLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('aws_yearly_info_service_get_aws_stn_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('aws_yearly_info_service_get_aws_stn_lst_tbl', params, use_sample=use_sample)) - def aws_yearly_info_service_get_note( + async def aws_yearly_info_service_get_note( self, *, use_sample: bool = False, @@ -7936,9 +7901,9 @@ def aws_yearly_info_service_get_note( Path: /api/typ02/openApi/AwsYearlyInfoService/getNote 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('aws_yearly_info_service_get_note', params, use_sample=use_sample) + return (await self.call_endpoint('aws_yearly_info_service_get_note', params, use_sample=use_sample)) - def aws_mtly_info_service_get_daily_aws_data( + async def aws_mtly_info_service_get_daily_aws_data( self, *, use_sample: bool = False, @@ -7950,9 +7915,9 @@ def aws_mtly_info_service_get_daily_aws_data( /api/typ02/openApi/AwsMtlyInfoService/getDailyAwsData 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('aws_mtly_info_service_get_daily_aws_data', params, use_sample=use_sample) + return (await self.call_endpoint('aws_mtly_info_service_get_daily_aws_data', params, use_sample=use_sample)) - def aws_mtly_info_service_get_mm_sumry( + async def aws_mtly_info_service_get_mm_sumry( self, *, use_sample: bool = False, @@ -7963,9 +7928,9 @@ def aws_mtly_info_service_get_mm_sumry( Path: /api/typ02/openApi/AwsMtlyInfoService/getMmSumry 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('aws_mtly_info_service_get_mm_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('aws_mtly_info_service_get_mm_sumry', params, use_sample=use_sample)) - def aws_mtly_info_service_get_aws_stn_lst_tbl( + async def aws_mtly_info_service_get_aws_stn_lst_tbl( self, *, use_sample: bool = False, @@ -7977,9 +7942,9 @@ def aws_mtly_info_service_get_aws_stn_lst_tbl( /api/typ02/openApi/AwsMtlyInfoService/getAwsStnLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('aws_mtly_info_service_get_aws_stn_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('aws_mtly_info_service_get_aws_stn_lst_tbl', params, use_sample=use_sample)) - def aws_mtly_info_service_get_note( + async def aws_mtly_info_service_get_note( self, *, use_sample: bool = False, @@ -7990,9 +7955,9 @@ def aws_mtly_info_service_get_note( Path: /api/typ02/openApi/AwsMtlyInfoService/getNote 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('aws_mtly_info_service_get_note', params, use_sample=use_sample) + return (await self.call_endpoint('aws_mtly_info_service_get_note', params, use_sample=use_sample)) - def alw_aws_aws_ww_pnt( + async def alw_aws_aws_ww_pnt( self, *, use_sample: bool = False, @@ -8003,9 +7968,9 @@ def alw_aws_aws_ww_pnt( Path: /api/typ03/php/alw/aws/aws_ww_pnt.php 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('alw_aws_aws_ww_pnt', params, use_sample=use_sample) + return (await self.call_endpoint('alw_aws_aws_ww_pnt', params, use_sample=use_sample)) - def aws3_nph_aws_day_img1( + async def aws3_nph_aws_day_img1( self, *, use_sample: bool = False, @@ -8016,9 +7981,9 @@ def aws3_nph_aws_day_img1( Path: /api/typ03/cgi/aws3/nph-aws_day_img1 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, zoom_level, zoom_x, zoom_y""" - return self.call_endpoint('aws3_nph_aws_day_img1', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_aws_day_img1', params, use_sample=use_sample)) - def aws3_nph_aws_min_img1( + async def aws3_nph_aws_min_img1( self, *, use_sample: bool = False, @@ -8029,9 +7994,9 @@ def aws3_nph_aws_min_img1( Path: /api/typ03/cgi/aws3/nph-aws_min_img1 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov, _DT""" - return self.call_endpoint('aws3_nph_aws_min_img1', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_aws_min_img1', params, use_sample=use_sample)) - def aws3_nph_aws_min_img2( + async def aws3_nph_aws_min_img2( self, *, use_sample: bool = False, @@ -8042,9 +8007,9 @@ def aws3_nph_aws_min_img2( Path: /api/typ03/cgi/aws3/nph-aws_min_img2 파라미터: obs, tm, val, stn, obj, ws_ms, map, grid, legend""" - return self.call_endpoint('aws3_nph_aws_min_img2', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_aws_min_img2', params, use_sample=use_sample)) - def alw_aws_aws_obs_pnt( + async def alw_aws_aws_obs_pnt( self, *, use_sample: bool = False, @@ -8055,9 +8020,9 @@ def alw_aws_aws_obs_pnt( Path: /api/typ03/php/alw/aws/aws_obs_pnt.php 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv""" - return self.call_endpoint('alw_aws_aws_obs_pnt', params, use_sample=use_sample) + return (await self.call_endpoint('alw_aws_aws_obs_pnt', params, use_sample=use_sample)) - def alw_sea_sea_obs_pnt( + async def alw_sea_sea_obs_pnt( self, *, use_sample: bool = False, @@ -8068,9 +8033,9 @@ def alw_sea_sea_obs_pnt( Path: /api/typ03/php/alw/sea/sea_obs_pnt.php 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov, _DT""" - return self.call_endpoint('alw_sea_sea_obs_pnt', params, use_sample=use_sample) + return (await self.call_endpoint('alw_sea_sea_obs_pnt', params, use_sample=use_sample)) - def aws3_nph_awsm_tms_h06( + async def aws3_nph_awsm_tms_h06( self, *, use_sample: bool = False, @@ -8081,9 +8046,9 @@ def aws3_nph_awsm_tms_h06( Path: /api/typ03/cgi/aws3/nph-awsm_tms_h06 파라미터: arg1, arg2, arg3, arg4, arg5, arg6, _DT""" - return self.call_endpoint('aws3_nph_awsm_tms_h06', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_awsm_tms_h06', params, use_sample=use_sample)) - def aws3_nph_awsm_tms_h12( + async def aws3_nph_awsm_tms_h12( self, *, use_sample: bool = False, @@ -8094,9 +8059,9 @@ def aws3_nph_awsm_tms_h12( Path: /api/typ03/cgi/aws3/nph-awsm_tms_h12 파라미터: arg1, arg2, arg3, arg4, arg5, arg6, arg7, _DT""" - return self.call_endpoint('aws3_nph_awsm_tms_h12', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_awsm_tms_h12', params, use_sample=use_sample)) - def aws2_nph_awsm_tms_h24( + async def aws2_nph_awsm_tms_h24( self, *, use_sample: bool = False, @@ -8107,9 +8072,9 @@ def aws2_nph_awsm_tms_h24( Path: /api/typ03/cgi/aws2/nph-awsm_tms_h24 파라미터: arg1, arg2, arg3, arg4, arg5, arg6""" - return self.call_endpoint('aws2_nph_awsm_tms_h24', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_nph_awsm_tms_h24', params, use_sample=use_sample)) - def aws2_nph_awsm_tms_d02( + async def aws2_nph_awsm_tms_d02( self, *, use_sample: bool = False, @@ -8120,9 +8085,9 @@ def aws2_nph_awsm_tms_d02( Path: /api/typ03/cgi/aws2/nph-awsm_tms_d02 파라미터: arg1, arg2, arg3, arg4, arg5, arg6, arg7""" - return self.call_endpoint('aws2_nph_awsm_tms_d02', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_nph_awsm_tms_d02', params, use_sample=use_sample)) - def aws2_nph_awsm_tms_d04( + async def aws2_nph_awsm_tms_d04( self, *, use_sample: bool = False, @@ -8133,9 +8098,9 @@ def aws2_nph_awsm_tms_d04( Path: /api/typ03/cgi/aws2/nph-awsm_tms_d04 파라미터: arg1, arg2, arg3, arg4, arg5, arg6""" - return self.call_endpoint('aws2_nph_awsm_tms_d04', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_nph_awsm_tms_d04', params, use_sample=use_sample)) - def aws2_nph_awsm_tms_d08( + async def aws2_nph_awsm_tms_d08( self, *, use_sample: bool = False, @@ -8146,9 +8111,9 @@ def aws2_nph_awsm_tms_d08( Path: /api/typ03/cgi/aws2/nph-awsm_tms_d08 파라미터: arg1, arg2, arg3, arg4, arg5, arg6""" - return self.call_endpoint('aws2_nph_awsm_tms_d08', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_nph_awsm_tms_d08', params, use_sample=use_sample)) - def aws2_nph_awsm_tms_d12( + async def aws2_nph_awsm_tms_d12( self, *, use_sample: bool = False, @@ -8159,9 +8124,9 @@ def aws2_nph_awsm_tms_d12( Path: /api/typ03/cgi/aws2/nph-awsm_tms_d12 파라미터: arg1, arg2, arg3, arg4, arg5, arg6""" - return self.call_endpoint('aws2_nph_awsm_tms_d12', params, use_sample=use_sample) + return (await self.call_endpoint('aws2_nph_awsm_tms_d12', params, use_sample=use_sample)) - def aws3_nph_aws_day_imgp1( + async def aws3_nph_aws_day_imgp1( self, *, use_sample: bool = False, @@ -8172,9 +8137,9 @@ def aws3_nph_aws_day_imgp1( Path: /api/typ03/cgi/aws3/nph-aws_day_imgp1 파라미터: PROJ, map, grid, itv, dataDtlCd, obs, stn, size, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('aws3_nph_aws_day_imgp1', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_aws_day_imgp1', params, use_sample=use_sample)) - def aws3_nph_aws_min_imgp1( + async def aws3_nph_aws_min_imgp1( self, *, use_sample: bool = False, @@ -8185,9 +8150,9 @@ def aws3_nph_aws_min_imgp1( Path: /api/typ03/cgi/aws3/nph-aws_min_imgp1 파라미터: PROJ, map, grid, itv, dataDtlCd, obs, stn, size, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('aws3_nph_aws_min_imgp1', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_aws_min_imgp1', params, use_sample=use_sample)) - def aws3_nph_aws_min_imgp2( + async def aws3_nph_aws_min_imgp2( self, *, use_sample: bool = False, @@ -8198,9 +8163,9 @@ def aws3_nph_aws_min_imgp2( Path: /api/typ03/cgi/aws3/nph-aws_min_imgp2 파라미터: PROJ, map, grid, itv, dataDtlCd, obs, stn, size, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('aws3_nph_aws_min_imgp2', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_aws_min_imgp2', params, use_sample=use_sample)) - def sts_ta( + async def sts_ta( self, *, use_sample: bool = False, @@ -8211,9 +8176,9 @@ def sts_ta( Path: /api/typ01/url/sts_ta.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_ta', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ta', params, use_sample=use_sample)) - def sts_ta_2( + async def sts_ta_2( self, *, use_sample: bool = False, @@ -8224,9 +8189,9 @@ def sts_ta_2( Path: /api/typ01/url/sts_ta.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_ta_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ta_2', params, use_sample=use_sample)) - def sts_si( + async def sts_si( self, *, use_sample: bool = False, @@ -8237,9 +8202,9 @@ def sts_si( Path: /api/typ01/url/sts_si.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_si', params, use_sample=use_sample) + return (await self.call_endpoint('sts_si', params, use_sample=use_sample)) - def sts_si_2( + async def sts_si_2( self, *, use_sample: bool = False, @@ -8250,9 +8215,9 @@ def sts_si_2( Path: /api/typ01/url/sts_si.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_si_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_si_2', params, use_sample=use_sample)) - def sts_ss( + async def sts_ss( self, *, use_sample: bool = False, @@ -8263,9 +8228,9 @@ def sts_ss( Path: /api/typ01/url/sts_ss.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_ss', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ss', params, use_sample=use_sample)) - def sts_ss_2( + async def sts_ss_2( self, *, use_sample: bool = False, @@ -8276,9 +8241,9 @@ def sts_ss_2( Path: /api/typ01/url/sts_ss.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_ss_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ss_2', params, use_sample=use_sample)) - def sts_pa( + async def sts_pa( self, *, use_sample: bool = False, @@ -8289,9 +8254,9 @@ def sts_pa( Path: /api/typ01/url/sts_pa.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_pa', params, use_sample=use_sample) + return (await self.call_endpoint('sts_pa', params, use_sample=use_sample)) - def sts_pa_2( + async def sts_pa_2( self, *, use_sample: bool = False, @@ -8302,9 +8267,9 @@ def sts_pa_2( Path: /api/typ01/url/sts_pa.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_pa_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_pa_2', params, use_sample=use_sample)) - def sts_wind( + async def sts_wind( self, *, use_sample: bool = False, @@ -8314,9 +8279,9 @@ def sts_wind( Path: /api/typ01/url/sts_wind.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_wind', params, use_sample=use_sample) + return (await self.call_endpoint('sts_wind', params, use_sample=use_sample)) - def sts_wind_2( + async def sts_wind_2( self, *, use_sample: bool = False, @@ -8327,9 +8292,9 @@ def sts_wind_2( Path: /api/typ01/url/sts_wind.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_wind_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_wind_2', params, use_sample=use_sample)) - def sts_td( + async def sts_td( self, *, use_sample: bool = False, @@ -8339,9 +8304,9 @@ def sts_td( Path: /api/typ01/url/sts_td.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_td', params, use_sample=use_sample) + return (await self.call_endpoint('sts_td', params, use_sample=use_sample)) - def sts_td_2( + async def sts_td_2( self, *, use_sample: bool = False, @@ -8352,9 +8317,9 @@ def sts_td_2( Path: /api/typ01/url/sts_td.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_td_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_td_2', params, use_sample=use_sample)) - def sts_ts( + async def sts_ts( self, *, use_sample: bool = False, @@ -8364,9 +8329,9 @@ def sts_ts( Path: /api/typ01/url/sts_ts.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_ts', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ts', params, use_sample=use_sample)) - def sts_ts_2( + async def sts_ts_2( self, *, use_sample: bool = False, @@ -8377,9 +8342,9 @@ def sts_ts_2( Path: /api/typ01/url/sts_ts.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_ts_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ts_2', params, use_sample=use_sample)) - def sts_tg( + async def sts_tg( self, *, use_sample: bool = False, @@ -8389,9 +8354,9 @@ def sts_tg( Path: /api/typ01/url/sts_tg.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_tg', params, use_sample=use_sample) + return (await self.call_endpoint('sts_tg', params, use_sample=use_sample)) - def sts_tg_2( + async def sts_tg_2( self, *, use_sample: bool = False, @@ -8402,9 +8367,9 @@ def sts_tg_2( Path: /api/typ01/url/sts_tg.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_tg_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_tg_2', params, use_sample=use_sample)) - def sts_te( + async def sts_te( self, *, use_sample: bool = False, @@ -8414,9 +8379,9 @@ def sts_te( Path: /api/typ01/url/sts_te.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_te', params, use_sample=use_sample) + return (await self.call_endpoint('sts_te', params, use_sample=use_sample)) - def sts_te_2( + async def sts_te_2( self, *, use_sample: bool = False, @@ -8427,9 +8392,9 @@ def sts_te_2( Path: /api/typ01/url/sts_te.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_te_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_te_2', params, use_sample=use_sample)) - def sts_rhm( + async def sts_rhm( self, *, use_sample: bool = False, @@ -8439,9 +8404,9 @@ def sts_rhm( Path: /api/typ01/url/sts_rhm.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_rhm', params, use_sample=use_sample) + return (await self.call_endpoint('sts_rhm', params, use_sample=use_sample)) - def sts_rhm_2( + async def sts_rhm_2( self, *, use_sample: bool = False, @@ -8452,9 +8417,9 @@ def sts_rhm_2( Path: /api/typ01/url/sts_rhm.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_rhm_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_rhm_2', params, use_sample=use_sample)) - def sts_pv( + async def sts_pv( self, *, use_sample: bool = False, @@ -8464,9 +8429,9 @@ def sts_pv( Path: /api/typ01/url/sts_pv.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_pv', params, use_sample=use_sample) + return (await self.call_endpoint('sts_pv', params, use_sample=use_sample)) - def sts_pv_2( + async def sts_pv_2( self, *, use_sample: bool = False, @@ -8477,9 +8442,9 @@ def sts_pv_2( Path: /api/typ01/url/sts_pv.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_pv_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_pv_2', params, use_sample=use_sample)) - def sts_cloud( + async def sts_cloud( self, *, use_sample: bool = False, @@ -8489,9 +8454,9 @@ def sts_cloud( Path: /api/typ01/url/sts_cloud.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_cloud', params, use_sample=use_sample) + return (await self.call_endpoint('sts_cloud', params, use_sample=use_sample)) - def sts_cloud_2( + async def sts_cloud_2( self, *, use_sample: bool = False, @@ -8502,9 +8467,9 @@ def sts_cloud_2( Path: /api/typ01/url/sts_cloud.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_cloud_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_cloud_2', params, use_sample=use_sample)) - def sts_vs( + async def sts_vs( self, *, use_sample: bool = False, @@ -8514,9 +8479,9 @@ def sts_vs( Path: /api/typ01/url/sts_vs.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_vs', params, use_sample=use_sample) + return (await self.call_endpoint('sts_vs', params, use_sample=use_sample)) - def sts_vs_2( + async def sts_vs_2( self, *, use_sample: bool = False, @@ -8527,9 +8492,9 @@ def sts_vs_2( Path: /api/typ01/url/sts_vs.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_vs_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_vs_2', params, use_sample=use_sample)) - def sts_rn( + async def sts_rn( self, *, use_sample: bool = False, @@ -8539,9 +8504,9 @@ def sts_rn( Path: /api/typ01/url/sts_rn.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_rn', params, use_sample=use_sample) + return (await self.call_endpoint('sts_rn', params, use_sample=use_sample)) - def sts_rn_2( + async def sts_rn_2( self, *, use_sample: bool = False, @@ -8552,9 +8517,9 @@ def sts_rn_2( Path: /api/typ01/url/sts_rn.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_rn_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_rn_2', params, use_sample=use_sample)) - def sts_sd( + async def sts_sd( self, *, use_sample: bool = False, @@ -8564,9 +8529,9 @@ def sts_sd( Path: /api/typ01/url/sts_sd.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_sd', params, use_sample=use_sample) + return (await self.call_endpoint('sts_sd', params, use_sample=use_sample)) - def sts_sd_2( + async def sts_sd_2( self, *, use_sample: bool = False, @@ -8577,9 +8542,9 @@ def sts_sd_2( Path: /api/typ01/url/sts_sd.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_sd_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_sd_2', params, use_sample=use_sample)) - def sts_ev( + async def sts_ev( self, *, use_sample: bool = False, @@ -8589,9 +8554,9 @@ def sts_ev( Path: /api/typ01/url/sts_ev.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_ev', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ev', params, use_sample=use_sample)) - def sts_ev_2( + async def sts_ev_2( self, *, use_sample: bool = False, @@ -8602,9 +8567,9 @@ def sts_ev_2( Path: /api/typ01/url/sts_ev.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_ev_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ev_2', params, use_sample=use_sample)) - def sts_ydst( + async def sts_ydst( self, *, use_sample: bool = False, @@ -8614,9 +8579,9 @@ def sts_ydst( Path: /api/typ01/url/sts_ydst.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_ydst', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ydst', params, use_sample=use_sample)) - def sts_ydst_2( + async def sts_ydst_2( self, *, use_sample: bool = False, @@ -8627,9 +8592,9 @@ def sts_ydst_2( Path: /api/typ01/url/sts_ydst.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_ydst_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_ydst_2', params, use_sample=use_sample)) - def sts_fog( + async def sts_fog( self, *, use_sample: bool = False, @@ -8639,9 +8604,9 @@ def sts_fog( Path: /api/typ01/url/sts_fog.php 파라미터: tm1, tm2, stn_id, help, disp""" - return self.call_endpoint('sts_fog', params, use_sample=use_sample) + return (await self.call_endpoint('sts_fog', params, use_sample=use_sample)) - def sts_fog_2( + async def sts_fog_2( self, *, use_sample: bool = False, @@ -8652,9 +8617,9 @@ def sts_fog_2( Path: /api/typ01/url/sts_fog.php 파라미터: tm1, tm2, lat, lon, help, disp""" - return self.call_endpoint('sts_fog_2', params, use_sample=use_sample) + return (await self.call_endpoint('sts_fog_2', params, use_sample=use_sample)) - def nko_sfctm( + async def nko_sfctm( self, *, use_sample: bool = False, @@ -8664,9 +8629,9 @@ def nko_sfctm( Path: /api/typ01/url/nko_sfctm.php 파라미터: tm, stn, help""" - return self.call_endpoint('nko_sfctm', params, use_sample=use_sample) + return (await self.call_endpoint('nko_sfctm', params, use_sample=use_sample)) - def sfc_nko_norm1( + async def sfc_nko_norm1( self, *, use_sample: bool = False, @@ -8677,9 +8642,9 @@ def sfc_nko_norm1( Path: /api/typ01/url/sfc_nko_norm1.php 파라미터: norm, tmst, stn, MM1, DD1, MM2, DD2""" - return self.call_endpoint('sfc_nko_norm1', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_nko_norm1', params, use_sample=use_sample)) - def kma_pm10( + async def kma_pm10( self, *, use_sample: bool = False, @@ -8689,9 +8654,9 @@ def kma_pm10( Path: /api/typ01/url/kma_pm10.php 파라미터: tm1, tm2""" - return self.call_endpoint('kma_pm10', params, use_sample=use_sample) + return (await self.call_endpoint('kma_pm10', params, use_sample=use_sample)) - def stn_pm10_inf( + async def stn_pm10_inf( self, *, use_sample: bool = False, @@ -8702,9 +8667,9 @@ def stn_pm10_inf( Path: /api/typ01/url/stn_pm10_inf.php 파라미터: inf, stn, tm, help""" - return self.call_endpoint('stn_pm10_inf', params, use_sample=use_sample) + return (await self.call_endpoint('stn_pm10_inf', params, use_sample=use_sample)) - def dst_pm10_tm( + async def dst_pm10_tm( self, *, use_sample: bool = False, @@ -8715,9 +8680,9 @@ def dst_pm10_tm( Path: /api/typ01/url/dst_pm10_tm.php 파라미터: tm, org, stn, data, mode, help""" - return self.call_endpoint('dst_pm10_tm', params, use_sample=use_sample) + return (await self.call_endpoint('dst_pm10_tm', params, use_sample=use_sample)) - def dst_pm10_tm_2( + async def dst_pm10_tm_2( self, *, use_sample: bool = False, @@ -8727,9 +8692,9 @@ def dst_pm10_tm_2( Path: /api/typ01/url/dst_pm10_tm.php 파라미터: tm, org""" - return self.call_endpoint('dst_pm10_tm_2', params, use_sample=use_sample) + return (await self.call_endpoint('dst_pm10_tm_2', params, use_sample=use_sample)) - def dst_pm10_hr( + async def dst_pm10_hr( self, *, use_sample: bool = False, @@ -8740,9 +8705,9 @@ def dst_pm10_hr( Path: /api/typ01/url/dst_pm10_hr.php 파라미터: tm, org, stn, mode, help""" - return self.call_endpoint('dst_pm10_hr', params, use_sample=use_sample) + return (await self.call_endpoint('dst_pm10_hr', params, use_sample=use_sample)) - def dst_pm10_hr_2( + async def dst_pm10_hr_2( self, *, use_sample: bool = False, @@ -8752,9 +8717,9 @@ def dst_pm10_hr_2( Path: /api/typ01/url/dst_pm10_hr.php 파라미터: tm, org""" - return self.call_endpoint('dst_pm10_hr_2', params, use_sample=use_sample) + return (await self.call_endpoint('dst_pm10_hr_2', params, use_sample=use_sample)) - def ydst_info_service_get_ydst_satlit_img( + async def ydst_info_service_get_ydst_satlit_img( self, *, use_sample: bool = False, @@ -8766,9 +8731,9 @@ def ydst_info_service_get_ydst_satlit_img( /api/typ02/openApi/YdstInfoService/getYdstSatlitImg 파라미터: pageNo, numOfRows, dataType, time""" - return self.call_endpoint('ydst_info_service_get_ydst_satlit_img', params, use_sample=use_sample) + return (await self.call_endpoint('ydst_info_service_get_ydst_satlit_img', params, use_sample=use_sample)) - def ydst_info_service_get_ydst_obs( + async def ydst_info_service_get_ydst_obs( self, *, use_sample: bool = False, @@ -8779,9 +8744,9 @@ def ydst_info_service_get_ydst_obs( Path: /api/typ02/openApi/YdstInfoService/getYdstObs 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('ydst_info_service_get_ydst_obs', params, use_sample=use_sample) + return (await self.call_endpoint('ydst_info_service_get_ydst_obs', params, use_sample=use_sample)) - def ydst_info_service_get_ydst_sfc_chart( + async def ydst_info_service_get_ydst_sfc_chart( self, *, use_sample: bool = False, @@ -8793,9 +8758,9 @@ def ydst_info_service_get_ydst_sfc_chart( /api/typ02/openApi/YdstInfoService/getYdstSfcChart 파라미터: pageNo, numOfRows, dataType, time""" - return self.call_endpoint('ydst_info_service_get_ydst_sfc_chart', params, use_sample=use_sample) + return (await self.call_endpoint('ydst_info_service_get_ydst_sfc_chart', params, use_sample=use_sample)) - def stn_snow( + async def stn_snow( self, *, use_sample: bool = False, @@ -8805,9 +8770,9 @@ def stn_snow( Path: /api/typ01/url/stn_snow.php 파라미터: stn, tm, mode, help""" - return self.call_endpoint('stn_snow', params, use_sample=use_sample) + return (await self.call_endpoint('stn_snow', params, use_sample=use_sample)) - def kma_snow1( + async def kma_snow1( self, *, use_sample: bool = False, @@ -8817,9 +8782,9 @@ def kma_snow1( Path: /api/typ01/url/kma_snow1.php 파라미터: sd, tm, help""" - return self.call_endpoint('kma_snow1', params, use_sample=use_sample) + return (await self.call_endpoint('kma_snow1', params, use_sample=use_sample)) - def kma_snow2( + async def kma_snow2( self, *, use_sample: bool = False, @@ -8829,9 +8794,9 @@ def kma_snow2( Path: /api/typ01/url/kma_snow2.php 파라미터: tm, tm_st, snow, help""" - return self.call_endpoint('kma_snow2', params, use_sample=use_sample) + return (await self.call_endpoint('kma_snow2', params, use_sample=use_sample)) - def kma_snow_day( + async def kma_snow_day( self, *, use_sample: bool = False, @@ -8842,9 +8807,9 @@ def kma_snow_day( Path: /api/typ01/url/kma_snow_day.php 파라미터: sd, tm, tm_st, stn, snow, help""" - return self.call_endpoint('kma_snow_day', params, use_sample=use_sample) + return (await self.call_endpoint('kma_snow_day', params, use_sample=use_sample)) - def kma_snow_day_2( + async def kma_snow_day_2( self, *, use_sample: bool = False, @@ -8855,9 +8820,9 @@ def kma_snow_day_2( Path: /api/typ01/url/kma_snow_day.php 파라미터: sd, tm, tm_st, help""" - return self.call_endpoint('kma_snow_day_2', params, use_sample=use_sample) + return (await self.call_endpoint('kma_snow_day_2', params, use_sample=use_sample)) - def kma_sfctm_uv( + async def kma_sfctm_uv( self, *, use_sample: bool = False, @@ -8867,9 +8832,9 @@ def kma_sfctm_uv( Path: /api/typ01/url/kma_sfctm_uv.php 파라미터: tm, stn, help""" - return self.call_endpoint('kma_sfctm_uv', params, use_sample=use_sample) + return (await self.call_endpoint('kma_sfctm_uv', params, use_sample=use_sample)) - def aws_nph_aws_min_obj( + async def aws_nph_aws_min_obj( self, *, use_sample: bool = False, @@ -8880,9 +8845,9 @@ def aws_nph_aws_min_obj( Path: /api/typ01/cgi-bin/aws/nph-aws_min_obj 파라미터: obs, tm, obj, map, grid, stn, gov""" - return self.call_endpoint('aws_nph_aws_min_obj', params, use_sample=use_sample) + return (await self.call_endpoint('aws_nph_aws_min_obj', params, use_sample=use_sample)) - def aws_nph_sfc_obs_img( + async def aws_nph_sfc_obs_img( self, *, use_sample: bool = False, @@ -8893,9 +8858,9 @@ def aws_nph_sfc_obs_img( Path: /api/typ01/cgi-bin/aws/nph-sfc_obs_img 파라미터: tm, obs, acc, val, stn, obj, map, xp, yp, lon, lat, zoom, size, legend, lonlat, typ, wv, gov""" - return self.call_endpoint('aws_nph_sfc_obs_img', params, use_sample=use_sample) + return (await self.call_endpoint('aws_nph_sfc_obs_img', params, use_sample=use_sample)) - def sfc_ssn( + async def sfc_ssn( self, *, use_sample: bool = False, @@ -8906,9 +8871,9 @@ def sfc_ssn( Path: /api/typ01/url/sfc_ssn.php 파라미터: stn, tm1, tm2""" - return self.call_endpoint('sfc_ssn', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_ssn', params, use_sample=use_sample)) - def sfc_ssn_2( + async def sfc_ssn_2( self, *, use_sample: bool = False, @@ -8919,9 +8884,9 @@ def sfc_ssn_2( Path: /api/typ01/url/sfc_ssn.php 파라미터: stn, tm1, tm2, ssn""" - return self.call_endpoint('sfc_ssn_2', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_ssn_2', params, use_sample=use_sample)) - def sfc_ssn_norm( + async def sfc_ssn_norm( self, *, use_sample: bool = False, @@ -8932,9 +8897,9 @@ def sfc_ssn_norm( Path: /api/typ01/url/sfc_ssn_norm.php 파라미터: tmst, stn, MM1, DD1, MM2, DD2""" - return self.call_endpoint('sfc_ssn_norm', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_ssn_norm', params, use_sample=use_sample)) - def sfc_ssn_norm_2( + async def sfc_ssn_norm_2( self, *, use_sample: bool = False, @@ -8945,9 +8910,9 @@ def sfc_ssn_norm_2( Path: /api/typ01/url/sfc_ssn_norm.php 파라미터: stn, MM1, DD1, MM2, DD2, ssn""" - return self.call_endpoint('sfc_ssn_norm_2', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_ssn_norm_2', params, use_sample=use_sample)) - def stn_inf( + async def stn_inf( self, *, use_sample: bool = False, @@ -8957,9 +8922,9 @@ def stn_inf( Path: /api/typ01/url/stn_inf.php 파라미터: inf, stn, tm, help""" - return self.call_endpoint('stn_inf', params, use_sample=use_sample) + return (await self.call_endpoint('stn_inf', params, use_sample=use_sample)) - def sea_obs( + async def sea_obs( self, *, use_sample: bool = False, @@ -8969,9 +8934,9 @@ def sea_obs( Path: /api/typ01/url/sea_obs.php 파라미터: tm, stn, help""" - return self.call_endpoint('sea_obs', params, use_sample=use_sample) + return (await self.call_endpoint('sea_obs', params, use_sample=use_sample)) - def kma_buoy2( + async def kma_buoy2( self, *, use_sample: bool = False, @@ -8982,9 +8947,9 @@ def kma_buoy2( Path: /api/typ01/url/kma_buoy2.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('kma_buoy2', params, use_sample=use_sample) + return (await self.call_endpoint('kma_buoy2', params, use_sample=use_sample)) - def kma_buoy( + async def kma_buoy( self, *, use_sample: bool = False, @@ -8994,9 +8959,9 @@ def kma_buoy( Path: /api/typ01/url/kma_buoy.php 파라미터: tm, stn, help""" - return self.call_endpoint('kma_buoy', params, use_sample=use_sample) + return (await self.call_endpoint('kma_buoy', params, use_sample=use_sample)) - def sea_mtly_info_service_get_note( + async def sea_mtly_info_service_get_note( self, *, use_sample: bool = False, @@ -9007,9 +8972,9 @@ def sea_mtly_info_service_get_note( Path: /api/typ02/openApi/SeaMtlyInfoService/getNote 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_note', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_note', params, use_sample=use_sample)) - def sea_mtly_info_service_get_buoy_lst_tbl( + async def sea_mtly_info_service_get_buoy_lst_tbl( self, *, use_sample: bool = False, @@ -9021,9 +8986,9 @@ def sea_mtly_info_service_get_buoy_lst_tbl( /api/typ02/openApi/SeaMtlyInfoService/getBuoyLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_buoy_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_buoy_lst_tbl', params, use_sample=use_sample)) - def sea_mtly_info_service_get_lhaws_lst_tbl( + async def sea_mtly_info_service_get_lhaws_lst_tbl( self, *, use_sample: bool = False, @@ -9035,9 +9000,9 @@ def sea_mtly_info_service_get_lhaws_lst_tbl( /api/typ02/openApi/SeaMtlyInfoService/getLhawsLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_lhaws_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_lhaws_lst_tbl', params, use_sample=use_sample)) - def sea_mtly_info_service_get_wave_buoy_lst_tbl( + async def sea_mtly_info_service_get_wave_buoy_lst_tbl( self, *, use_sample: bool = False, @@ -9049,9 +9014,9 @@ def sea_mtly_info_service_get_wave_buoy_lst_tbl( /api/typ02/openApi/SeaMtlyInfoService/getWaveBuoyLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_wave_buoy_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_wave_buoy_lst_tbl', params, use_sample=use_sample)) - def sea_mtly_info_service_get_obs_open_year( + async def sea_mtly_info_service_get_obs_open_year( self, *, use_sample: bool = False, @@ -9061,9 +9026,9 @@ def sea_mtly_info_service_get_obs_open_year( Path: /api/typ02/openApi/SeaMtlyInfoService/getObsOpenYear 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_obs_open_year', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_obs_open_year', params, use_sample=use_sample)) - def sea_mtly_info_service_get_buoy_mm_sumry( + async def sea_mtly_info_service_get_buoy_mm_sumry( self, *, use_sample: bool = False, @@ -9075,9 +9040,9 @@ def sea_mtly_info_service_get_buoy_mm_sumry( /api/typ02/openApi/SeaMtlyInfoService/getBuoyMmSumry 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_buoy_mm_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_buoy_mm_sumry', params, use_sample=use_sample)) - def sea_mtly_info_service_get_buoy_mm_sumry2( + async def sea_mtly_info_service_get_buoy_mm_sumry2( self, *, use_sample: bool = False, @@ -9089,9 +9054,9 @@ def sea_mtly_info_service_get_buoy_mm_sumry2( /api/typ02/openApi/SeaMtlyInfoService/getBuoyMmSumry2 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_buoy_mm_sumry2', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_buoy_mm_sumry2', params, use_sample=use_sample)) - def sea_mtly_info_service_get_daily_buoy( + async def sea_mtly_info_service_get_daily_buoy( self, *, use_sample: bool = False, @@ -9103,9 +9068,9 @@ def sea_mtly_info_service_get_daily_buoy( /api/typ02/openApi/SeaMtlyInfoService/getDailyBuoy 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('sea_mtly_info_service_get_daily_buoy', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_daily_buoy', params, use_sample=use_sample)) - def sea_mtly_info_service_get_lhaws_mm_sumry( + async def sea_mtly_info_service_get_lhaws_mm_sumry( self, *, use_sample: bool = False, @@ -9117,9 +9082,9 @@ def sea_mtly_info_service_get_lhaws_mm_sumry( /api/typ02/openApi/SeaMtlyInfoService/getLhawsMmSumry 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_lhaws_mm_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_lhaws_mm_sumry', params, use_sample=use_sample)) - def sea_mtly_info_service_get_lhaws_mm_sumry2( + async def sea_mtly_info_service_get_lhaws_mm_sumry2( self, *, use_sample: bool = False, @@ -9131,9 +9096,9 @@ def sea_mtly_info_service_get_lhaws_mm_sumry2( /api/typ02/openApi/SeaMtlyInfoService/getLhawsMmSumry2 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_lhaws_mm_sumry2', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_lhaws_mm_sumry2', params, use_sample=use_sample)) - def sea_mtly_info_service_get_daily_lhaws( + async def sea_mtly_info_service_get_daily_lhaws( self, *, use_sample: bool = False, @@ -9145,9 +9110,9 @@ def sea_mtly_info_service_get_daily_lhaws( /api/typ02/openApi/SeaMtlyInfoService/getDailyLhaws 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('sea_mtly_info_service_get_daily_lhaws', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_daily_lhaws', params, use_sample=use_sample)) - def sea_mtly_info_service_get_wave_buoy_mm_sumry( + async def sea_mtly_info_service_get_wave_buoy_mm_sumry( self, *, use_sample: bool = False, @@ -9159,9 +9124,9 @@ def sea_mtly_info_service_get_wave_buoy_mm_sumry( /api/typ02/openApi/SeaMtlyInfoService/getWaveBuoyMmSumry 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_wave_buoy_mm_sumry', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_wave_buoy_mm_sumry', params, use_sample=use_sample)) - def sea_mtly_info_service_get_wave_buoy_mm_sumry2( + async def sea_mtly_info_service_get_wave_buoy_mm_sumry2( self, *, use_sample: bool = False, @@ -9173,9 +9138,9 @@ def sea_mtly_info_service_get_wave_buoy_mm_sumry2( /api/typ02/openApi/SeaMtlyInfoService/getWaveBuoyMmSumry2 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sea_mtly_info_service_get_wave_buoy_mm_sumry2', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_wave_buoy_mm_sumry2', params, use_sample=use_sample)) - def sea_mtly_info_service_get_daily_wave_buoy( + async def sea_mtly_info_service_get_daily_wave_buoy( self, *, use_sample: bool = False, @@ -9187,9 +9152,9 @@ def sea_mtly_info_service_get_daily_wave_buoy( /api/typ02/openApi/SeaMtlyInfoService/getDailyWaveBuoy 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('sea_mtly_info_service_get_daily_wave_buoy', params, use_sample=use_sample) + return (await self.call_endpoint('sea_mtly_info_service_get_daily_wave_buoy', params, use_sample=use_sample)) - def aws3_nph_sea_obs_imgp1( + async def aws3_nph_sea_obs_imgp1( self, *, use_sample: bool = False, @@ -9199,9 +9164,9 @@ def aws3_nph_sea_obs_imgp1( Path: /api/typ03/cgi/aws3/nph-sea_obs_imgp1 파라미터: 없음""" - return self.call_endpoint('aws3_nph_sea_obs_imgp1', params, use_sample=use_sample) + return (await self.call_endpoint('aws3_nph_sea_obs_imgp1', params, use_sample=use_sample)) - def kma_lhaws( + async def kma_lhaws( self, *, use_sample: bool = False, @@ -9211,9 +9176,9 @@ def kma_lhaws( Path: /api/typ01/url/kma_lhaws.php 파라미터: tm, stn, help""" - return self.call_endpoint('kma_lhaws', params, use_sample=use_sample) + return (await self.call_endpoint('kma_lhaws', params, use_sample=use_sample)) - def kma_lhaws2( + async def kma_lhaws2( self, *, use_sample: bool = False, @@ -9223,9 +9188,9 @@ def kma_lhaws2( Path: /api/typ01/url/kma_lhaws2.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('kma_lhaws2', params, use_sample=use_sample) + return (await self.call_endpoint('kma_lhaws2', params, use_sample=use_sample)) - def kma_kship( + async def kma_kship( self, *, use_sample: bool = False, @@ -9235,9 +9200,9 @@ def kma_kship( Path: /api/typ01/url/kma_kship.php 파라미터: tm, stn, help""" - return self.call_endpoint('kma_kship', params, use_sample=use_sample) + return (await self.call_endpoint('kma_kship', params, use_sample=use_sample)) - def upp_temp( + async def upp_temp( self, *, use_sample: bool = False, @@ -9248,9 +9213,9 @@ def upp_temp( Path: /api/typ01/url/upp_temp.php 파라미터: tm, stn, pa, help""" - return self.call_endpoint('upp_temp', params, use_sample=use_sample) + return (await self.call_endpoint('upp_temp', params, use_sample=use_sample)) - def sea_kship_temp( + async def sea_kship_temp( self, *, use_sample: bool = False, @@ -9261,9 +9226,9 @@ def sea_kship_temp( Path: /api/typ01/url/sea_kship_temp.php 파라미터: tm, stn, pa, help""" - return self.call_endpoint('sea_kship_temp', params, use_sample=use_sample) + return (await self.call_endpoint('sea_kship_temp', params, use_sample=use_sample)) - def upp_mbl_temp( + async def upp_mbl_temp( self, *, use_sample: bool = False, @@ -9274,9 +9239,9 @@ def upp_mbl_temp( Path: /api/typ01/url/upp_mbl_temp.php 파라미터: tm, stn, pa, help""" - return self.call_endpoint('upp_mbl_temp', params, use_sample=use_sample) + return (await self.call_endpoint('upp_mbl_temp', params, use_sample=use_sample)) - def upp_raw_max( + async def upp_raw_max( self, *, use_sample: bool = False, @@ -9286,9 +9251,9 @@ def upp_raw_max( Path: /api/typ01/url/upp_raw_max.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('upp_raw_max', params, use_sample=use_sample) + return (await self.call_endpoint('upp_raw_max', params, use_sample=use_sample)) - def upp_idx( + async def upp_idx( self, *, use_sample: bool = False, @@ -9298,9 +9263,9 @@ def upp_idx( Path: /api/typ01/url/upp_idx.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('upp_idx', params, use_sample=use_sample) + return (await self.call_endpoint('upp_idx', params, use_sample=use_sample)) - def upp_mtly_info_service_get_note( + async def upp_mtly_info_service_get_note( self, *, use_sample: bool = False, @@ -9311,9 +9276,9 @@ def upp_mtly_info_service_get_note( Path: /api/typ02/openApi/UppMtlyInfoService/getNote 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('upp_mtly_info_service_get_note', params, use_sample=use_sample) + return (await self.call_endpoint('upp_mtly_info_service_get_note', params, use_sample=use_sample)) - def upp_mtly_info_service_get_upp_lst_tbl( + async def upp_mtly_info_service_get_upp_lst_tbl( self, *, use_sample: bool = False, @@ -9323,9 +9288,9 @@ def upp_mtly_info_service_get_upp_lst_tbl( Path: /api/typ02/openApi/UppMtlyInfoService/getUppLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('upp_mtly_info_service_get_upp_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('upp_mtly_info_service_get_upp_lst_tbl', params, use_sample=use_sample)) - def upp_mtly_info_service_get_std_isbrsf_value( + async def upp_mtly_info_service_get_std_isbrsf_value( self, *, use_sample: bool = False, @@ -9337,9 +9302,9 @@ def upp_mtly_info_service_get_std_isbrsf_value( /api/typ02/openApi/UppMtlyInfoService/getStdIsbrsfValue 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('upp_mtly_info_service_get_std_isbrsf_value', params, use_sample=use_sample) + return (await self.call_endpoint('upp_mtly_info_service_get_std_isbrsf_value', params, use_sample=use_sample)) - def upp_mtly_info_service_get_max_wind( + async def upp_mtly_info_service_get_max_wind( self, *, use_sample: bool = False, @@ -9349,9 +9314,9 @@ def upp_mtly_info_service_get_max_wind( Path: /api/typ02/openApi/UppMtlyInfoService/getMaxWind 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('upp_mtly_info_service_get_max_wind', params, use_sample=use_sample) + return (await self.call_endpoint('upp_mtly_info_service_get_max_wind', params, use_sample=use_sample)) - def upp_mtly_info_service_get_ta_hm_level( + async def upp_mtly_info_service_get_ta_hm_level( self, *, use_sample: bool = False, @@ -9363,9 +9328,9 @@ def upp_mtly_info_service_get_ta_hm_level( /api/typ02/openApi/UppMtlyInfoService/getTaHmLevel 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('upp_mtly_info_service_get_ta_hm_level', params, use_sample=use_sample) + return (await self.call_endpoint('upp_mtly_info_service_get_ta_hm_level', params, use_sample=use_sample)) - def upp_mtly_info_service_get_wind_level( + async def upp_mtly_info_service_get_wind_level( self, *, use_sample: bool = False, @@ -9375,9 +9340,9 @@ def upp_mtly_info_service_get_wind_level( Path: /api/typ02/openApi/UppMtlyInfoService/getWindLevel 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('upp_mtly_info_service_get_wind_level', params, use_sample=use_sample) + return (await self.call_endpoint('upp_mtly_info_service_get_wind_level', params, use_sample=use_sample)) - def kma_wpf( + async def kma_wpf( self, *, use_sample: bool = False, @@ -9388,9 +9353,9 @@ def kma_wpf( Path: /api/typ01/url/kma_wpf.php 파라미터: tm, stn, mode, help""" - return self.call_endpoint('kma_wpf', params, use_sample=use_sample) + return (await self.call_endpoint('kma_wpf', params, use_sample=use_sample)) - def kma_wpf_file_down( + async def kma_wpf_file_down( self, *, use_sample: bool = False, @@ -9401,9 +9366,9 @@ def kma_wpf_file_down( Path: /api/typ01/url/kma_wpf_file_down.php 파라미터: wpf, stn, tm""" - return self.call_endpoint('kma_wpf_file_down', params, use_sample=use_sample) + return (await self.call_endpoint('kma_wpf_file_down', params, use_sample=use_sample)) - def stn_wpf( + async def stn_wpf( self, *, use_sample: bool = False, @@ -9414,9 +9379,9 @@ def stn_wpf( Path: /api/typ01/url/stn_wpf.php 파라미터: tm, stn, raw, help""" - return self.call_endpoint('stn_wpf', params, use_sample=use_sample) + return (await self.call_endpoint('stn_wpf', params, use_sample=use_sample)) - def rdr_stn_file_list( + async def rdr_stn_file_list( self, *, use_sample: bool = False, @@ -9427,9 +9392,9 @@ def rdr_stn_file_list( Path: /api/typ01/url/rdr_stn_file_list.php 파라미터: stn, rdr, tm, size""" - return self.call_endpoint('rdr_stn_file_list', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_stn_file_list', params, use_sample=use_sample)) - def rdr_cmp_file_list( + async def rdr_cmp_file_list( self, *, use_sample: bool = False, @@ -9440,9 +9405,9 @@ def rdr_cmp_file_list( Path: /api/typ01/url/rdr_cmp_file_list.php 파라미터: cmp, tm""" - return self.call_endpoint('rdr_cmp_file_list', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_cmp_file_list', params, use_sample=use_sample)) - def rdr_cmp_inf( + async def rdr_cmp_inf( self, *, use_sample: bool = False, @@ -9453,9 +9418,9 @@ def rdr_cmp_inf( Path: /api/typ01/cgi-bin/url/nph- rdr_cmp_inf 파라미터: tm, cmp, qcd""" - return self.call_endpoint('rdr_cmp_inf', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_cmp_inf', params, use_sample=use_sample)) - def rdr_cmp1_api( + async def rdr_cmp1_api( self, *, use_sample: bool = False, @@ -9466,9 +9431,9 @@ def rdr_cmp1_api( Path: /api/typ01/cgi-bin/url/nph- rdr_cmp1_api 파라미터: tm, cmp, qcd, obs, map, disp""" - return self.call_endpoint('rdr_cmp1_api', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_cmp1_api', params, use_sample=use_sample)) - def rdr_cmp1_api_2( + async def rdr_cmp1_api_2( self, *, use_sample: bool = False, @@ -9479,9 +9444,9 @@ def rdr_cmp1_api_2( Path: /api/typ01/cgi-bin/url/nph- rdr_cmp1_api 파라미터: tm, cmp, qcd, obs, acc, map, disp""" - return self.call_endpoint('rdr_cmp1_api_2', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_cmp1_api_2', params, use_sample=use_sample)) - def wthr_radar_info_service_get_comp_cappi_qcd_all( + async def wthr_radar_info_service_get_comp_cappi_qcd_all( self, *, use_sample: bool = False, @@ -9493,9 +9458,9 @@ def wthr_radar_info_service_get_comp_cappi_qcd_all( /api/typ02/openApi/WthrRadarInfoService/getCompCappiQcdAll 파라미터: pageNo, numOfRows, dataType, dateTime, compType, dataTypeCd""" - return self.call_endpoint('wthr_radar_info_service_get_comp_cappi_qcd_all', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_radar_info_service_get_comp_cappi_qcd_all', params, use_sample=use_sample)) - def wthr_radar_info_service_get_comp_cappi_qcd_area( + async def wthr_radar_info_service_get_comp_cappi_qcd_area( self, *, use_sample: bool = False, @@ -9507,9 +9472,9 @@ def wthr_radar_info_service_get_comp_cappi_qcd_area( /api/typ02/openApi/WthrRadarInfoService/getCompCappiQcdArea 파라미터: pageNo, numOfRows, dataType, dateTime, compType, dataTypeCd, dongCode""" - return self.call_endpoint('wthr_radar_info_service_get_comp_cappi_qcd_area', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_radar_info_service_get_comp_cappi_qcd_area', params, use_sample=use_sample)) - def rdr_latlon_api( + async def rdr_latlon_api( self, *, use_sample: bool = False, @@ -9520,9 +9485,9 @@ def rdr_latlon_api( Path: /api/typ01/cgi-bin/url/nph- rdr_latlon_api 파라미터: cmp, latlon, disp""" - return self.call_endpoint('rdr_latlon_api', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_latlon_api', params, use_sample=use_sample)) - def rdr_latlon_file_down( + async def rdr_latlon_file_down( self, *, use_sample: bool = False, @@ -9533,9 +9498,9 @@ def rdr_latlon_file_down( Path: /api/typ01/url/rdr_latlon_file_down.php 파라미터: cmp""" - return self.call_endpoint('rdr_latlon_file_down', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_latlon_file_down', params, use_sample=use_sample)) - def rdr_cmp_file( + async def rdr_cmp_file( self, *, use_sample: bool = False, @@ -9545,9 +9510,9 @@ def rdr_cmp_file( Path: /api/typ04/url/rdr_cmp_file.php 파라미터: tm, data, cmp""" - return self.call_endpoint('rdr_cmp_file', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_cmp_file', params, use_sample=use_sample)) - def wthr_radar_info_service_get_site_cappi_qcd_all( + async def wthr_radar_info_service_get_site_cappi_qcd_all( self, *, use_sample: bool = False, @@ -9559,9 +9524,9 @@ def wthr_radar_info_service_get_site_cappi_qcd_all( /api/typ02/openApi/WthrRadarInfoService/getSiteCappiQcdAll 파라미터: pageNo, numOfRows, dataType, dateTime, dataTypeCd, siteCode, sweep""" - return self.call_endpoint('wthr_radar_info_service_get_site_cappi_qcd_all', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_radar_info_service_get_site_cappi_qcd_all', params, use_sample=use_sample)) - def wthr_radar_info_service_get_site_cappi_qcd_area( + async def wthr_radar_info_service_get_site_cappi_qcd_area( self, *, use_sample: bool = False, @@ -9573,9 +9538,9 @@ def wthr_radar_info_service_get_site_cappi_qcd_area( /api/typ02/openApi/WthrRadarInfoService/getSiteCappiQcdArea 파라미터: pageNo, numOfRows, dataType, dateTime, dataTypeCd, siteCode, sweep, dongCode""" - return self.call_endpoint('wthr_radar_info_service_get_site_cappi_qcd_area', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_radar_info_service_get_site_cappi_qcd_area', params, use_sample=use_sample)) - def rdr_nph_rdr_cmp1_img( + async def rdr_nph_rdr_cmp1_img( self, *, use_sample: bool = False, @@ -9587,9 +9552,9 @@ def rdr_nph_rdr_cmp1_img( 파라미터: tm, cmp, qcd, obs, color, aws, acc, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('rdr_nph_rdr_cmp1_img', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_rdr_cmp1_img', params, use_sample=use_sample)) - def rdr_nph_rdr_wis_ana_img( + async def rdr_nph_rdr_wis_ana_img( self, *, use_sample: bool = False, @@ -9601,9 +9566,9 @@ def rdr_nph_rdr_wis_ana_img( rdr_wis_ana_img 파라미터: tm, obs, wv, ht, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('rdr_nph_rdr_wis_ana_img', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_rdr_wis_ana_img', params, use_sample=use_sample)) - def rdr_nph_rdr_obs_ta_h_img( + async def rdr_nph_rdr_obs_ta_h_img( self, *, use_sample: bool = False, @@ -9613,9 +9578,9 @@ def rdr_nph_rdr_obs_ta_h_img( Path: /api/typ03/cgi/rdr/nph-rdr_obs_taH_img 파라미터: tm, obs, ta1, ta2, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('rdr_nph_rdr_obs_ta_h_img', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_rdr_obs_ta_h_img', params, use_sample=use_sample)) - def rdr_nph_qpf_ana_img( + async def rdr_nph_qpf_ana_img( self, *, use_sample: bool = False, @@ -9626,9 +9591,9 @@ def rdr_nph_qpf_ana_img( Path: /api/typ03/cgi/rdr/nph-qpf_ana_img 파라미터: tm, qpf, eva, option, ef, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('rdr_nph_qpf_ana_img', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_qpf_ana_img', params, use_sample=use_sample)) - def rdr_nph_rdr_cmp1_imgp( + async def rdr_nph_rdr_cmp1_imgp( self, *, use_sample: bool = False, @@ -9642,9 +9607,9 @@ def rdr_nph_rdr_cmp1_imgp( size, an_frn, an_itv, river, road, city, gis_auto, stnname, ctrl, dataDtlCd, data1, data2, data3, overlay, color, effect, height, qpf, ef, legend, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('rdr_nph_rdr_cmp1_imgp', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_rdr_cmp1_imgp', params, use_sample=use_sample)) - def rdr_nph_rdr_wis_ana_imgp( + async def rdr_nph_rdr_wis_ana_imgp( self, *, use_sample: bool = False, @@ -9659,9 +9624,9 @@ def rdr_nph_rdr_wis_ana_imgp( winnum, rand, size, an_frn, an_itv, river, road, city, gis_auto, stnname, ctrl, dataDtlCd, data1, data2, data3, overlay, color, effect, height, qpf, ef, eva, option, legend, acc, sms, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('rdr_nph_rdr_wis_ana_imgp', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_rdr_wis_ana_imgp', params, use_sample=use_sample)) - def rdr_nph_rdr_obs_ta_h_imgp( + async def rdr_nph_rdr_obs_ta_h_imgp( self, *, use_sample: bool = False, @@ -9676,9 +9641,9 @@ def rdr_nph_rdr_obs_ta_h_imgp( winnum, rand, size, an_frn, an_itv, river, road, city, gis_auto, stnname, ctrl, dataDtlCd, data1, data2, data3, overlay, color, effect, height, qpf, ef, eva, option, legend, acc, sms, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('rdr_nph_rdr_obs_ta_h_imgp', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_rdr_obs_ta_h_imgp', params, use_sample=use_sample)) - def rdr_nph_qpf_ana_imgp( + async def rdr_nph_qpf_ana_imgp( self, *, use_sample: bool = False, @@ -9692,9 +9657,9 @@ def rdr_nph_qpf_ana_imgp( size, an_frn, an_itv, river, road, city, gis_auto, stnname, ctrl, dataDtlCd, data1, data2, data3, overlay, color, effect, height, qpf, ef, eva, option, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('rdr_nph_qpf_ana_imgp', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_nph_qpf_ana_imgp', params, use_sample=use_sample)) - def rdr_uf_list( + async def rdr_uf_list( self, *, use_sample: bool = False, @@ -9705,9 +9670,9 @@ def rdr_uf_list( Path: /api/typ01/url/rdr_uf_list.php 파라미터: tm, dtm, stn, qcd, disp, help""" - return self.call_endpoint('rdr_uf_list', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_uf_list', params, use_sample=use_sample)) - def rdr_file_list( + async def rdr_file_list( self, *, use_sample: bool = False, @@ -9718,9 +9683,9 @@ def rdr_file_list( Path: /api/typ01/url/rdr_file_list.php 파라미터: rdr, qcd, tm""" - return self.call_endpoint('rdr_file_list', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_file_list', params, use_sample=use_sample)) - def rdr_uf_inf( + async def rdr_uf_inf( self, *, use_sample: bool = False, @@ -9731,9 +9696,9 @@ def rdr_uf_inf( Path: /api/typ01/cgi-bin/url/nph-rdr_uf_inf 파라미터: tm, stn, qcd, help""" - return self.call_endpoint('rdr_uf_inf', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_uf_inf', params, use_sample=use_sample)) - def rdr_uf_data( + async def rdr_uf_data( self, *, use_sample: bool = False, @@ -9744,9 +9709,9 @@ def rdr_uf_data( Path: /api/typ01/cgi-bin/url/nph-rdr_uf_data 파라미터: tm, stn, qcd, vol, sw, mode, help""" - return self.call_endpoint('rdr_uf_data', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_uf_data', params, use_sample=use_sample)) - def rdr_file_down( + async def rdr_file_down( self, *, use_sample: bool = False, @@ -9757,9 +9722,9 @@ def rdr_file_down( Path: /api/typ01/url/rdr_file_down.php 파라미터: rdr, stn, qcd, tm""" - return self.call_endpoint('rdr_file_down', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_file_down', params, use_sample=use_sample)) - def rdr_file_down_nc( + async def rdr_file_down_nc( self, *, use_sample: bool = False, @@ -9770,9 +9735,9 @@ def rdr_file_down_nc( Path: /api/typ01/url/rdr_file_down_nc.php 파라미터: rdr, stn, qcd, tm""" - return self.call_endpoint('rdr_file_down_nc', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_file_down_nc', params, use_sample=use_sample)) - def rdr_site_file( + async def rdr_site_file( self, *, use_sample: bool = False, @@ -9783,9 +9748,9 @@ def rdr_site_file( Path: /api/typ04/url/rdr_site_file.php 파라미터: tm, data, stn""" - return self.call_endpoint('rdr_site_file', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_site_file', params, use_sample=use_sample)) - def rdr_cmp_aws_pt_data( + async def rdr_cmp_aws_pt_data( self, *, use_sample: bool = False, @@ -9795,9 +9760,9 @@ def rdr_cmp_aws_pt_data( Path: /api/typ01/cgi-bin/url/nph-rdr_cmp_aws_pt_data 파라미터: tm1, tm2, itv, qcd, cmp, stn, help""" - return self.call_endpoint('rdr_cmp_aws_pt_data', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_cmp_aws_pt_data', params, use_sample=use_sample)) - def rdr_cmp_aws_all_pt_data( + async def rdr_cmp_aws_all_pt_data( self, *, use_sample: bool = False, @@ -9808,9 +9773,9 @@ def rdr_cmp_aws_all_pt_data( Path: /api/typ01/cgi-bin/url/nph- rdr_cmp_aws_all_pt_data 파라미터: tm, qcd, cmp, help""" - return self.call_endpoint('rdr_cmp_aws_all_pt_data', params, use_sample=use_sample) + return (await self.call_endpoint('rdr_cmp_aws_all_pt_data', params, use_sample=use_sample)) - def lgt_kma_np1( + async def lgt_kma_np1( self, *, use_sample: bool = False, @@ -9821,9 +9786,9 @@ def lgt_kma_np1( Path: /api/typ01/url/lgt_kma_np1.php 파라미터: tm1, tm2, help""" - return self.call_endpoint('lgt_kma_np1', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_kma_np1', params, use_sample=use_sample)) - def lgt_kma_np2( + async def lgt_kma_np2( self, *, use_sample: bool = False, @@ -9834,9 +9799,9 @@ def lgt_kma_np2( Path: /api/typ01/url/lgt_kma_np2.php 파라미터: tm1, tm2, help""" - return self.call_endpoint('lgt_kma_np2', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_kma_np2', params, use_sample=use_sample)) - def lgt_kma_np3( + async def lgt_kma_np3( self, *, use_sample: bool = False, @@ -9847,9 +9812,9 @@ def lgt_kma_np3( Path: /api/typ01/url/lgt_kma_np3.php 파라미터: tm1, tm2, help""" - return self.call_endpoint('lgt_kma_np3', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_kma_np3', params, use_sample=use_sample)) - def lgt_kma_nx1( + async def lgt_kma_nx1( self, *, use_sample: bool = False, @@ -9859,9 +9824,9 @@ def lgt_kma_nx1( Path: /api/typ01/url/lgt_kma_nx1.php 파라미터: tm1, tm2, help""" - return self.call_endpoint('lgt_kma_nx1', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_kma_nx1', params, use_sample=use_sample)) - def lgt_pnt( + async def lgt_pnt( self, *, use_sample: bool = False, @@ -9871,9 +9836,9 @@ def lgt_pnt( Path: /api/typ01/url/lgt_pnt.php 파라미터: tm, itv""" - return self.call_endpoint('lgt_pnt', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_pnt', params, use_sample=use_sample)) - def lgt_pnt_2( + async def lgt_pnt_2( self, *, use_sample: bool = False, @@ -9884,9 +9849,9 @@ def lgt_pnt_2( Path: /api/typ01/url/lgt_pnt.php 파라미터: tm, itv, lon, lat, range""" - return self.call_endpoint('lgt_pnt_2', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_pnt_2', params, use_sample=use_sample)) - def lgt_pnt_3( + async def lgt_pnt_3( self, *, use_sample: bool = False, @@ -9897,9 +9862,9 @@ def lgt_pnt_3( Path: /api/typ01/url/lgt_pnt.php 파라미터: tm, itv, lon, lat, range, gc""" - return self.call_endpoint('lgt_pnt_3', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_pnt_3', params, use_sample=use_sample)) - def lgt_stn( + async def lgt_stn( self, *, use_sample: bool = False, @@ -9909,9 +9874,9 @@ def lgt_stn( Path: /api/typ01/url/lgt_stn.php 파라미터: tp, tm, range""" - return self.call_endpoint('lgt_stn', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_stn', params, use_sample=use_sample)) - def lgt_nph_lgt_str_img( + async def lgt_nph_lgt_str_img( self, *, use_sample: bool = False, @@ -9922,9 +9887,9 @@ def lgt_nph_lgt_str_img( Path: /api/typ03/cgi/lgt/nph-lgt_str_img 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('lgt_nph_lgt_str_img', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_nph_lgt_str_img', params, use_sample=use_sample)) - def lgt_nph_lgt_ana_img( + async def lgt_nph_lgt_ana_img( self, *, use_sample: bool = False, @@ -9935,9 +9900,9 @@ def lgt_nph_lgt_ana_img( Path: /api/typ03/cgi/lgt/nph-lgt_ana_img 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('lgt_nph_lgt_ana_img', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_nph_lgt_ana_img', params, use_sample=use_sample)) - def lgt_nph_lgt_dst_img( + async def lgt_nph_lgt_dst_img( self, *, use_sample: bool = False, @@ -9948,9 +9913,9 @@ def lgt_nph_lgt_dst_img( Path: /api/typ03/cgi/lgt/nph-lgt_dst_img 파라미터: obs, tm, val, stn, obj, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('lgt_nph_lgt_dst_img', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_nph_lgt_dst_img', params, use_sample=use_sample)) - def lgt_admndst_cnt( + async def lgt_admndst_cnt( self, *, use_sample: bool = False, @@ -9961,9 +9926,9 @@ def lgt_admndst_cnt( Path: /api/typ01/url/lgt_admndst_cnt.php 파라미터: admdst_dv, unit, interval, tm, disp, help""" - return self.call_endpoint('lgt_admndst_cnt', params, use_sample=use_sample) + return (await self.call_endpoint('lgt_admndst_cnt', params, use_sample=use_sample)) - def wethr_basic_info_service_get_radar_obs_stn( + async def wethr_basic_info_service_get_radar_obs_stn( self, *, use_sample: bool = False, @@ -9975,9 +9940,9 @@ def wethr_basic_info_service_get_radar_obs_stn( /api/typ02/openApi/WethrBasicInfoService/getRadarObsStn 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('wethr_basic_info_service_get_radar_obs_stn', params, use_sample=use_sample) + return (await self.call_endpoint('wethr_basic_info_service_get_radar_obs_stn', params, use_sample=use_sample)) - def nr016_fd_data( + async def nr016_fd_data( self, *, use_sample: bool = False, @@ -9988,9 +9953,9 @@ def nr016_fd_data( Path: /api/typ05/api/GK2A/LE1B/NR016/FD/data 파라미터: date""" - return self.call_endpoint('nr016_fd_data', params, use_sample=use_sample) + return (await self.call_endpoint('nr016_fd_data', params, use_sample=use_sample)) - def sw038_tp_data_list( + async def sw038_tp_data_list( self, *, use_sample: bool = False, @@ -10000,9 +9965,9 @@ def sw038_tp_data_list( Path: /api/typ05/api/GK2A/LE1B/SW038/TP/dataList 파라미터: sDate, eDate""" - return self.call_endpoint('sw038_tp_data_list', params, use_sample=use_sample) + return (await self.call_endpoint('sw038_tp_data_list', params, use_sample=use_sample)) - def vi004_ea_image( + async def vi004_ea_image( self, *, use_sample: bool = False, @@ -10013,9 +9978,9 @@ def vi004_ea_image( Path: /api/typ05/api/GK2A/LE1B/VI004/EA/image 파라미터: date""" - return self.call_endpoint('vi004_ea_image', params, use_sample=use_sample) + return (await self.call_endpoint('vi004_ea_image', params, use_sample=use_sample)) - def vi005_fd_image_list( + async def vi005_fd_image_list( self, *, use_sample: bool = False, @@ -10025,9 +9990,9 @@ def vi005_fd_image_list( Path: /api/typ05/api/GK2A/LE1B/VI005/FD/imageList 파라미터: sDate, eDate""" - return self.call_endpoint('vi005_fd_image_list', params, use_sample=use_sample) + return (await self.call_endpoint('vi005_fd_image_list', params, use_sample=use_sample)) - def ci_ela_data( + async def ci_ela_data( self, *, use_sample: bool = False, @@ -10037,9 +10002,9 @@ def ci_ela_data( Path: /api/typ05/api/GK2A/LE2/CI/ELA/data 파라미터: date""" - return self.call_endpoint('ci_ela_data', params, use_sample=use_sample) + return (await self.call_endpoint('ci_ela_data', params, use_sample=use_sample)) - def so2_d_ko_data_list( + async def so2_d_ko_data_list( self, *, use_sample: bool = False, @@ -10049,9 +10014,9 @@ def so2_d_ko_data_list( Path: /api/typ05/api/GK2A/LE2/SO2D/KO/dataList 파라미터: sDate, eDate""" - return self.call_endpoint('so2_d_ko_data_list', params, use_sample=use_sample) + return (await self.call_endpoint('so2_d_ko_data_list', params, use_sample=use_sample)) - def cld_ea_image( + async def cld_ea_image( self, *, use_sample: bool = False, @@ -10062,9 +10027,9 @@ def cld_ea_image( Path: /api/typ05/api/GK2A/LE2/CLD/EA/image 파라미터: date""" - return self.call_endpoint('cld_ea_image', params, use_sample=use_sample) + return (await self.call_endpoint('cld_ea_image', params, use_sample=use_sample)) - def rr_ea_image_list( + async def rr_ea_image_list( self, *, use_sample: bool = False, @@ -10075,9 +10040,9 @@ def rr_ea_image_list( Path: /api/typ05/api/GK2A/LE2/RR/EA/imageList 파라미터: sDate, eDate""" - return self.call_endpoint('rr_ea_image_list', params, use_sample=use_sample) + return (await self.call_endpoint('rr_ea_image_list', params, use_sample=use_sample)) - def pd_e_1_m_na_data( + async def pd_e_1_m_na_data( self, *, use_sample: bool = False, @@ -10088,9 +10053,9 @@ def pd_e_1_m_na_data( Path: /api/typ05/api/GK2A/LV1/PD-E-1M/NA/data 파라미터: date""" - return self.call_endpoint('pd_e_1_m_na_data', params, use_sample=use_sample) + return (await self.call_endpoint('pd_e_1_m_na_data', params, use_sample=use_sample)) - def pd_e_1_m_na_data_list( + async def pd_e_1_m_na_data_list( self, *, use_sample: bool = False, @@ -10100,9 +10065,9 @@ def pd_e_1_m_na_data_list( Path: /api/typ05/api/GK2A/LV1/PD-E-1M/NA/dataList 파라미터: sDate, eDate""" - return self.call_endpoint('pd_e_1_m_na_data_list', params, use_sample=use_sample) + return (await self.call_endpoint('pd_e_1_m_na_data_list', params, use_sample=use_sample)) - def sat_nph_sat_ana_txt( + async def sat_nph_sat_ana_txt( self, *, use_sample: bool = False, @@ -10113,9 +10078,9 @@ def sat_nph_sat_ana_txt( Path: /api/typ01/cgi-bin/sat/nph- sat_ana_txt 파라미터: tm, obs, help""" - return self.call_endpoint('sat_nph_sat_ana_txt', params, use_sample=use_sample) + return (await self.call_endpoint('sat_nph_sat_ana_txt', params, use_sample=use_sample)) - def sat_nph_sat_ana_img( + async def sat_nph_sat_ana_img( self, *, use_sample: bool = False, @@ -10126,9 +10091,9 @@ def sat_nph_sat_ana_img( Path: /api/typ01/cgi-bin/sat/nph- sat_ana_img 파라미터: obs, tm, size, sat, map, xp, yp, zoom, scn""" - return self.call_endpoint('sat_nph_sat_ana_img', params, use_sample=use_sample) + return (await self.call_endpoint('sat_nph_sat_ana_img', params, use_sample=use_sample)) - def sat_file_down2( + async def sat_file_down2( self, *, use_sample: bool = False, @@ -10139,9 +10104,9 @@ def sat_file_down2( Path: /api/typ01/url/sat_file_down2.php 파라미터: lvl, dat, are, tm, typ""" - return self.call_endpoint('sat_file_down2', params, use_sample=use_sample) + return (await self.call_endpoint('sat_file_down2', params, use_sample=use_sample)) - def sat_file_list( + async def sat_file_list( self, *, use_sample: bool = False, @@ -10152,9 +10117,9 @@ def sat_file_list( Path: /api/typ01/url/sat_file_list.php 파라미터: sat, vars, area, fmt, tm, size, filter""" - return self.call_endpoint('sat_file_list', params, use_sample=use_sample) + return (await self.call_endpoint('sat_file_list', params, use_sample=use_sample)) - def sat_file_down2_2( + async def sat_file_down2_2( self, *, use_sample: bool = False, @@ -10165,9 +10130,9 @@ def sat_file_down2_2( Path: /api/typ01/url/sat_file_down2.php 파라미터: typ, lvl, are, chn, tm""" - return self.call_endpoint('sat_file_down2_2', params, use_sample=use_sample) + return (await self.call_endpoint('sat_file_down2_2', params, use_sample=use_sample)) - def sat_file_down2_3( + async def sat_file_down2_3( self, *, use_sample: bool = False, @@ -10178,9 +10143,9 @@ def sat_file_down2_3( Path: /api/typ01/url/sat_file_down2.php 파라미터: typ, lvl, are, dat, tm""" - return self.call_endpoint('sat_file_down2_3', params, use_sample=use_sample) + return (await self.call_endpoint('sat_file_down2_3', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2acla_area( + async def cloud_satlit_info_service_get_gk2acla_area( self, *, use_sample: bool = False, @@ -10192,9 +10157,9 @@ def cloud_satlit_info_service_get_gk2acla_area( /api/typ02/openApi/CloudSatlitInfoService/getGk2aclaArea 파라미터: pageNo, numOfRows, dataType, dateTime, resultType, dongCode""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2acla_area', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2acla_area', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2adcoew_area( + async def cloud_satlit_info_service_get_gk2adcoew_area( self, *, use_sample: bool = False, @@ -10206,9 +10171,9 @@ def cloud_satlit_info_service_get_gk2adcoew_area( /api/typ02/openApi/CloudSatlitInfoService/getGk2adcoewArea 파라미터: pageNo, numOfRows, dataType, dateTime, resultType, dongCode""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2adcoew_area', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2adcoew_area', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2afog_area( + async def cloud_satlit_info_service_get_gk2afog_area( self, *, use_sample: bool = False, @@ -10220,9 +10185,9 @@ def cloud_satlit_info_service_get_gk2afog_area( /api/typ02/openApi/CloudSatlitInfoService/getGk2afogArea 파라미터: pageNo, numOfRows, dataType, dateTime, resultType, dongCode""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2afog_area', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2afog_area', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2aapps_area( + async def cloud_satlit_info_service_get_gk2aapps_area( self, *, use_sample: bool = False, @@ -10234,9 +10199,9 @@ def cloud_satlit_info_service_get_gk2aapps_area( /api/typ02/openApi/CloudSatlitInfoService/getGk2aappsArea 파라미터: pageNo, numOfRows, dataType, dateTime, resultType, dongCode""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2aapps_area', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2aapps_area', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2acld_area( + async def cloud_satlit_info_service_get_gk2acld_area( self, *, use_sample: bool = False, @@ -10248,9 +10213,9 @@ def cloud_satlit_info_service_get_gk2acld_area( /api/typ02/openApi/CloudSatlitInfoService/getGk2acldArea 파라미터: pageNo, numOfRows, dataType, dateTime, resultType, dongCode""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2acld_area', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2acld_area', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2acla_all( + async def cloud_satlit_info_service_get_gk2acla_all( self, *, use_sample: bool = False, @@ -10262,9 +10227,9 @@ def cloud_satlit_info_service_get_gk2acla_all( /api/typ02/openApi/CloudSatlitInfoService/getGk2aclaAll 파라미터: pageNo, numOfRows, dataType, dateTime, resultType""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2acla_all', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2acla_all', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2adcoew_all( + async def cloud_satlit_info_service_get_gk2adcoew_all( self, *, use_sample: bool = False, @@ -10276,9 +10241,9 @@ def cloud_satlit_info_service_get_gk2adcoew_all( /api/typ02/openApi/CloudSatlitInfoService/getGk2adcoewAll 파라미터: pageNo, numOfRows, dataType, dateTime, resultType""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2adcoew_all', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2adcoew_all', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2afog_all( + async def cloud_satlit_info_service_get_gk2afog_all( self, *, use_sample: bool = False, @@ -10290,9 +10255,9 @@ def cloud_satlit_info_service_get_gk2afog_all( /api/typ02/openApi/CloudSatlitInfoService/getGk2afogAll 파라미터: pageNo, numOfRows, dataType, dateTime, resultType""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2afog_all', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2afog_all', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2aapps_all( + async def cloud_satlit_info_service_get_gk2aapps_all( self, *, use_sample: bool = False, @@ -10304,9 +10269,9 @@ def cloud_satlit_info_service_get_gk2aapps_all( /api/typ02/openApi/CloudSatlitInfoService/getGk2aappsAll 파라미터: pageNo, numOfRows, dataType, dateTime, resultType""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2aapps_all', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2aapps_all', params, use_sample=use_sample)) - def cloud_satlit_info_service_get_gk2acld_all( + async def cloud_satlit_info_service_get_gk2acld_all( self, *, use_sample: bool = False, @@ -10318,9 +10283,9 @@ def cloud_satlit_info_service_get_gk2acld_all( /api/typ02/openApi/CloudSatlitInfoService/getGk2acldAll 파라미터: pageNo, numOfRows, dataType, dateTime, resultType""" - return self.call_endpoint('cloud_satlit_info_service_get_gk2acld_all', params, use_sample=use_sample) + return (await self.call_endpoint('cloud_satlit_info_service_get_gk2acld_all', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_ir_all( + async def wthr_satlit_info_service_get_gk2a_ir_all( self, *, use_sample: bool = False, @@ -10332,9 +10297,9 @@ def wthr_satlit_info_service_get_gk2a_ir_all( /api/typ02/openApi/WthrSatlitInfoService/getGk2aIrAll 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_ir_all', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_ir_all', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_nr_all( + async def wthr_satlit_info_service_get_gk2a_nr_all( self, *, use_sample: bool = False, @@ -10346,9 +10311,9 @@ def wthr_satlit_info_service_get_gk2a_nr_all( /api/typ02/openApi/WthrSatlitInfoService/getGk2aNrAll 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_nr_all', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_nr_all', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_sw_all( + async def wthr_satlit_info_service_get_gk2a_sw_all( self, *, use_sample: bool = False, @@ -10360,9 +10325,9 @@ def wthr_satlit_info_service_get_gk2a_sw_all( /api/typ02/openApi/WthrSatlitInfoService/getGk2aSwAll 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_sw_all', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_sw_all', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_vi_all( + async def wthr_satlit_info_service_get_gk2a_vi_all( self, *, use_sample: bool = False, @@ -10374,9 +10339,9 @@ def wthr_satlit_info_service_get_gk2a_vi_all( /api/typ02/openApi/WthrSatlitInfoService/getGk2aViAll 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_vi_all', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_vi_all', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_wv_all( + async def wthr_satlit_info_service_get_gk2a_wv_all( self, *, use_sample: bool = False, @@ -10388,9 +10353,9 @@ def wthr_satlit_info_service_get_gk2a_wv_all( /api/typ02/openApi/WthrSatlitInfoService/getGk2aWvAll 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_wv_all', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_wv_all', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_ir_area( + async def wthr_satlit_info_service_get_gk2a_ir_area( self, *, use_sample: bool = False, @@ -10402,9 +10367,9 @@ def wthr_satlit_info_service_get_gk2a_ir_area( /api/typ02/openApi/WthrSatlitInfoService/getGk2aIrArea 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType, dongCode""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_ir_area', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_ir_area', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_nr_area( + async def wthr_satlit_info_service_get_gk2a_nr_area( self, *, use_sample: bool = False, @@ -10416,9 +10381,9 @@ def wthr_satlit_info_service_get_gk2a_nr_area( /api/typ02/openApi/WthrSatlitInfoService/getGk2aNrArea 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType, dongCode""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_nr_area', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_nr_area', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_sw_area( + async def wthr_satlit_info_service_get_gk2a_sw_area( self, *, use_sample: bool = False, @@ -10430,9 +10395,9 @@ def wthr_satlit_info_service_get_gk2a_sw_area( /api/typ02/openApi/WthrSatlitInfoService/getGk2aSwArea 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType, dongCode""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_sw_area', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_sw_area', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_vi_area( + async def wthr_satlit_info_service_get_gk2a_vi_area( self, *, use_sample: bool = False, @@ -10444,9 +10409,9 @@ def wthr_satlit_info_service_get_gk2a_vi_area( /api/typ02/openApi/WthrSatlitInfoService/getGk2aViArea 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType, dongCode""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_vi_area', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_vi_area', params, use_sample=use_sample)) - def wthr_satlit_info_service_get_gk2a_wv_area( + async def wthr_satlit_info_service_get_gk2a_wv_area( self, *, use_sample: bool = False, @@ -10458,9 +10423,9 @@ def wthr_satlit_info_service_get_gk2a_wv_area( /api/typ02/openApi/WthrSatlitInfoService/getGk2aWvArea 파라미터: pageNo, numOfRows, dataType, dateTime, waveType, unitType, dongCode""" - return self.call_endpoint('wthr_satlit_info_service_get_gk2a_wv_area', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_satlit_info_service_get_gk2a_wv_area', params, use_sample=use_sample)) - def sat_nph_gk2a_img( + async def sat_nph_gk2a_img( self, *, use_sample: bool = False, @@ -10471,9 +10436,9 @@ def sat_nph_gk2a_img( Path: /api/typ03/cgi/sat/nph-gk2a_img 파라미터: tm, obs, map, grid, legend, size, itv, zoom_level, zoom_x, zoom_y, gov""" - return self.call_endpoint('sat_nph_gk2a_img', params, use_sample=use_sample) + return (await self.call_endpoint('sat_nph_gk2a_img', params, use_sample=use_sample)) - def sat_nph_gk2a_imgp( + async def sat_nph_gk2a_imgp( self, *, use_sample: bool = False, @@ -10488,9 +10453,9 @@ def sat_nph_gk2a_imgp( winnum, rand, size, an_frn, an_itv, river, road, city, gis_auto, stnname, ctrl, dataDtlCd, data1, data2, data3, overlay, color, effect, height, qpf, ef, band1, legend, scn, STARTX, STARTY, ENDX, ENDY, ZOOMLVL, selWs, tm, tm_st, tm_ed, tm2""" - return self.call_endpoint('sat_nph_gk2a_imgp', params, use_sample=use_sample) + return (await self.call_endpoint('sat_nph_gk2a_imgp', params, use_sample=use_sample)) - def gk2a_latlon_api( + async def gk2a_latlon_api( self, *, use_sample: bool = False, @@ -10501,9 +10466,9 @@ def gk2a_latlon_api( Path: /api/typ01/cgi-bin/url/nph- gk2a_latlon_api 파라미터: area, grid, latlon, disp""" - return self.call_endpoint('gk2a_latlon_api', params, use_sample=use_sample) + return (await self.call_endpoint('gk2a_latlon_api', params, use_sample=use_sample)) - def gk2a_latlon_file_down( + async def gk2a_latlon_file_down( self, *, use_sample: bool = False, @@ -10514,9 +10479,9 @@ def gk2a_latlon_file_down( Path: /api/typ01/url/gk2a_latlon_file_down.php 파라미터: area, grid""" - return self.call_endpoint('gk2a_latlon_file_down', params, use_sample=use_sample) + return (await self.call_endpoint('gk2a_latlon_file_down', params, use_sample=use_sample)) - def vi004_ea_data( + async def vi004_ea_data( self, *, use_sample: bool = False, @@ -10527,9 +10492,9 @@ def vi004_ea_data( Path: /api/typ05/api/GK2A/LE1B/VI004/EA/data 파라미터: date""" - return self.call_endpoint('vi004_ea_data', params, use_sample=use_sample) + return (await self.call_endpoint('vi004_ea_data', params, use_sample=use_sample)) - def vi004_ea_data_list( + async def vi004_ea_data_list( self, *, use_sample: bool = False, @@ -10540,9 +10505,9 @@ def vi004_ea_data_list( Path: /api/typ05/api/GK2A/LE1B/VI004/EA/dataList 파라미터: sDate, eDate""" - return self.call_endpoint('vi004_ea_data_list', params, use_sample=use_sample) + return (await self.call_endpoint('vi004_ea_data_list', params, use_sample=use_sample)) - def vi004_ea_image_list( + async def vi004_ea_image_list( self, *, use_sample: bool = False, @@ -10553,9 +10518,9 @@ def vi004_ea_image_list( Path: /api/typ05/api/GK2A/LE1B/VI004/EA/imageList 파라미터: sDate, eDate""" - return self.call_endpoint('vi004_ea_image_list', params, use_sample=use_sample) + return (await self.call_endpoint('vi004_ea_image_list', params, use_sample=use_sample)) - def sat_file_list_2( + async def sat_file_list_2( self, *, use_sample: bool = False, @@ -10566,9 +10531,9 @@ def sat_file_list_2( Path: /api/typ01/url/sat_file_list.php 파라미터: sat, fmt, tm""" - return self.call_endpoint('sat_file_list_2', params, use_sample=use_sample) + return (await self.call_endpoint('sat_file_list_2', params, use_sample=use_sample)) - def sat_data( + async def sat_data( self, *, use_sample: bool = False, @@ -10579,9 +10544,9 @@ def sat_data( Path: /api/typ01/cgi-bin/url/nph-sat_data 파라미터: sat, chn, tm, help""" - return self.call_endpoint('sat_data', params, use_sample=use_sample) + return (await self.call_endpoint('sat_data', params, use_sample=use_sample)) - def coms_pnt( + async def coms_pnt( self, *, use_sample: bool = False, @@ -10592,9 +10557,9 @@ def coms_pnt( Path: /api/typ01/cgi-bin/url/nph-coms_pnt 파라미터: tm1, tm2, obs, lon, lat, help""" - return self.call_endpoint('coms_pnt', params, use_sample=use_sample) + return (await self.call_endpoint('coms_pnt', params, use_sample=use_sample)) - def coms_pnt_vars( + async def coms_pnt_vars( self, *, use_sample: bool = False, @@ -10604,9 +10569,9 @@ def coms_pnt_vars( Path: /api/typ01/url/coms_pnt_vars.php 파라미터: tm1, tm2, lon, lat, help""" - return self.call_endpoint('coms_pnt_vars', params, use_sample=use_sample) + return (await self.call_endpoint('coms_pnt_vars', params, use_sample=use_sample)) - def coms_stns( + async def coms_stns( self, *, use_sample: bool = False, @@ -10616,9 +10581,9 @@ def coms_stns( Path: /api/typ01/cgi-bin/url/nph-coms_stns 파라미터: tm1, tm2, obs, stn, help""" - return self.call_endpoint('coms_stns', params, use_sample=use_sample) + return (await self.call_endpoint('coms_stns', params, use_sample=use_sample)) - def coms_stns_vars( + async def coms_stns_vars( self, *, use_sample: bool = False, @@ -10628,9 +10593,9 @@ def coms_stns_vars( Path: /api/typ01/url/coms_stns_vars.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('coms_stns_vars', params, use_sample=use_sample) + return (await self.call_endpoint('coms_stns_vars', params, use_sample=use_sample)) - def coms_stn_ca( + async def coms_stn_ca( self, *, use_sample: bool = False, @@ -10641,9 +10606,9 @@ def coms_stn_ca( Path: /api/typ01/cgi-bin/url/nph-coms_stn_ca 파라미터: tm, range, help""" - return self.call_endpoint('coms_stn_ca', params, use_sample=use_sample) + return (await self.call_endpoint('coms_stn_ca', params, use_sample=use_sample)) - def sat_coms_obs_file( + async def sat_coms_obs_file( self, *, use_sample: bool = False, @@ -10654,9 +10619,9 @@ def sat_coms_obs_file( Path: /api/typ04/url/sat_coms_obs_file.php 파라미터: tm, ch, map""" - return self.call_endpoint('sat_coms_obs_file', params, use_sample=use_sample) + return (await self.call_endpoint('sat_coms_obs_file', params, use_sample=use_sample)) - def eqk_now( + async def eqk_now( self, *, use_sample: bool = False, @@ -10666,9 +10631,9 @@ def eqk_now( Path: /api/typ01/url/eqk_now.php 파라미터: tm, disp, help""" - return self.call_endpoint('eqk_now', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_now', params, use_sample=use_sample)) - def eqk_list( + async def eqk_list( self, *, use_sample: bool = False, @@ -10678,9 +10643,9 @@ def eqk_list( Path: /api/typ01/url/eqk_list.php 파라미터: tm1, tm2, disp, help""" - return self.call_endpoint('eqk_list', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_list', params, use_sample=use_sample)) - def eqk_info_service_get_eqk_msg_list( + async def eqk_info_service_get_eqk_msg_list( self, *, use_sample: bool = False, @@ -10690,9 +10655,9 @@ def eqk_info_service_get_eqk_msg_list( Path: /api/typ02/openApi/EqkInfoService/getEqkMsgList 파라미터: pageNo, numOfRows, dataType, fromTmFc, toTmFc""" - return self.call_endpoint('eqk_info_service_get_eqk_msg_list', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_info_service_get_eqk_msg_list', params, use_sample=use_sample)) - def eqk_info_service_get_eqk_msg( + async def eqk_info_service_get_eqk_msg( self, *, use_sample: bool = False, @@ -10703,9 +10668,9 @@ def eqk_info_service_get_eqk_msg( Path: /api/typ02/openApi/EqkInfoService/getEqkMsg 파라미터: pageNo, numOfRows, dataType, fromTmFc, toTmFc""" - return self.call_endpoint('eqk_info_service_get_eqk_msg', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_info_service_get_eqk_msg', params, use_sample=use_sample)) - def eqk_url_new_noti_eqk( + async def eqk_url_new_noti_eqk( self, *, use_sample: bool = False, @@ -10716,9 +10681,9 @@ def eqk_url_new_noti_eqk( Path: /api/typ09/url/eqk/urlNewNotiEqk.do 파라미터: orderTy, orderCm""" - return self.call_endpoint('eqk_url_new_noti_eqk', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_url_new_noti_eqk', params, use_sample=use_sample)) - def eqk_url_new_noti_eqk_2( + async def eqk_url_new_noti_eqk_2( self, *, use_sample: bool = False, @@ -10729,9 +10694,9 @@ def eqk_url_new_noti_eqk_2( Path: /api/typ09/url/eqk/urlNewNotiEqk.do 파라미터: orderTy, frDate, laDate, msgCode, cntDiv, arDiv, eqArCd, nkDiv""" - return self.call_endpoint('eqk_url_new_noti_eqk_2', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_url_new_noti_eqk_2', params, use_sample=use_sample)) - def eqk_url_sec_eqk_list( + async def eqk_url_sec_eqk_list( self, *, use_sample: bool = False, @@ -10742,9 +10707,9 @@ def eqk_url_sec_eqk_list( Path: /api/typ09/url/eqk/urlSecEqkList.do 파라미터: orderTy, mTeqId, frDate, laDate, afDiv, frMagMl, laMagMl, type""" - return self.call_endpoint('eqk_url_sec_eqk_list', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_url_sec_eqk_list', params, use_sample=use_sample)) - def tsnm_url_tsnm_list( + async def tsnm_url_tsnm_list( self, *, use_sample: bool = False, @@ -10754,9 +10719,9 @@ def tsnm_url_tsnm_list( Path: /api/typ09/url/tsnm/urlTsnmList.do 파라미터: orderTy, orderCm""" - return self.call_endpoint('tsnm_url_tsnm_list', params, use_sample=use_sample) + return (await self.call_endpoint('tsnm_url_tsnm_list', params, use_sample=use_sample)) - def tsnm_url_tsnm_list_2( + async def tsnm_url_tsnm_list_2( self, *, use_sample: bool = False, @@ -10767,9 +10732,9 @@ def tsnm_url_tsnm_list_2( Path: /api/typ09/url/tsnm/urlTsnmList.do 파라미터: orderTy, frDate, laDate""" - return self.call_endpoint('tsnm_url_tsnm_list_2', params, use_sample=use_sample) + return (await self.call_endpoint('tsnm_url_tsnm_list_2', params, use_sample=use_sample)) - def eqk_info_service_get_tsunami_msg_list( + async def eqk_info_service_get_tsunami_msg_list( self, *, use_sample: bool = False, @@ -10781,9 +10746,9 @@ def eqk_info_service_get_tsunami_msg_list( /api/typ02/openApi/EqkInfoService/getTsunamiMsgList 파라미터: pageNo, numOfRows, dataType, fromTmFc, toTmFc""" - return self.call_endpoint('eqk_info_service_get_tsunami_msg_list', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_info_service_get_tsunami_msg_list', params, use_sample=use_sample)) - def eqk_info_service_get_tsunami_msg( + async def eqk_info_service_get_tsunami_msg( self, *, use_sample: bool = False, @@ -10793,9 +10758,9 @@ def eqk_info_service_get_tsunami_msg( Path: /api/typ02/openApi/EqkInfoService/getTsunamiMsg 파라미터: pageNo, numOfRows, dataType, fromTmFc, toTmFc""" - return self.call_endpoint('eqk_info_service_get_tsunami_msg', params, use_sample=use_sample) + return (await self.call_endpoint('eqk_info_service_get_tsunami_msg', params, use_sample=use_sample)) - def volc_select_volc_info_list( + async def volc_select_volc_info_list( self, *, use_sample: bool = False, @@ -10806,9 +10771,9 @@ def volc_select_volc_info_list( Path: /api/typ09/url/volc/selectVolcInfoList.do 파라미터: orderTy, orderCm""" - return self.call_endpoint('volc_select_volc_info_list', params, use_sample=use_sample) + return (await self.call_endpoint('volc_select_volc_info_list', params, use_sample=use_sample)) - def volc_select_volc_info_list_2( + async def volc_select_volc_info_list_2( self, *, use_sample: bool = False, @@ -10818,9 +10783,9 @@ def volc_select_volc_info_list_2( Path: /api/typ09/url/volc/selectVolcInfoList.do 파라미터: orderTy, frDate, laDate""" - return self.call_endpoint('volc_select_volc_info_list_2', params, use_sample=use_sample) + return (await self.call_endpoint('volc_select_volc_info_list_2', params, use_sample=use_sample)) - def typ_lst( + async def typ_lst( self, *, use_sample: bool = False, @@ -10830,9 +10795,9 @@ def typ_lst( Path: /api/typ01/url/typ_lst.php 파라미터: YY, disp, help""" - return self.call_endpoint('typ_lst', params, use_sample=use_sample) + return (await self.call_endpoint('typ_lst', params, use_sample=use_sample)) - def typ_data( + async def typ_data( self, *, use_sample: bool = False, @@ -10843,9 +10808,9 @@ def typ_data( Path: /api/typ01/url/typ_data.php 파라미터: YY, typ, seq, mode, disp, help""" - return self.call_endpoint('typ_data', params, use_sample=use_sample) + return (await self.call_endpoint('typ_data', params, use_sample=use_sample)) - def typ_now( + async def typ_now( self, *, use_sample: bool = False, @@ -10856,9 +10821,9 @@ def typ_now( Path: /api/typ01/url/typ_now.php 파라미터: tm, mode, disp, help""" - return self.call_endpoint('typ_now', params, use_sample=use_sample) + return (await self.call_endpoint('typ_now', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_typhoon_list( + async def sfc_yearly_info_service_get_typhoon_list( self, *, use_sample: bool = False, @@ -10870,9 +10835,9 @@ def sfc_yearly_info_service_get_typhoon_list( /api/typ02/openApi/SfcYearlyInfoService/getTyphoonList 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_get_typhoon_list', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_typhoon_list', params, use_sample=use_sample)) - def td_lst( + async def td_lst( self, *, use_sample: bool = False, @@ -10882,9 +10847,9 @@ def td_lst( Path: /api/typ01/url/td_lst.php 파라미터: YY, disp, help""" - return self.call_endpoint('td_lst', params, use_sample=use_sample) + return (await self.call_endpoint('td_lst', params, use_sample=use_sample)) - def td_data( + async def td_data( self, *, use_sample: bool = False, @@ -10895,9 +10860,9 @@ def td_data( Path: /api/typ01/url/td_data.php 파라미터: YY, td, seq, mode, disp, help""" - return self.call_endpoint('td_data', params, use_sample=use_sample) + return (await self.call_endpoint('td_data', params, use_sample=use_sample)) - def td_now( + async def td_now( self, *, use_sample: bool = False, @@ -10908,9 +10873,9 @@ def td_now( Path: /api/typ01/url/td_now.php 파라미터: tm, mode, disp, help""" - return self.call_endpoint('td_now', params, use_sample=use_sample) + return (await self.call_endpoint('td_now', params, use_sample=use_sample)) - def typ_besttrack( + async def typ_besttrack( self, *, use_sample: bool = False, @@ -10921,9 +10886,9 @@ def typ_besttrack( Path: /api/typ01/url/typ_besttrack.php 파라미터: year, grade, tcid, help""" - return self.call_endpoint('typ_besttrack', params, use_sample=use_sample) + return (await self.call_endpoint('typ_besttrack', params, use_sample=use_sample)) - def nwp_vars_down( + async def nwp_vars_down( self, *, use_sample: bool = False, @@ -10933,9 +10898,9 @@ def nwp_vars_down( Path: /api/typ06/url/nwp_vars_down.php 파라미터: nwp, sub, vars, pres, tmfc, ef, dataType""" - return self.call_endpoint('nwp_vars_down', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_vars_down', params, use_sample=use_sample)) - def kim_grib_xy_txt1( + async def kim_grib_xy_txt1( self, *, use_sample: bool = False, @@ -10946,9 +10911,9 @@ def kim_grib_xy_txt1( Path: /api/typ06/cgi-bin/url/nph- kim_grib_xy_txt1 파라미터: group, nwp, data, varn, level, tmfc, hf, disp""" - return self.call_endpoint('kim_grib_xy_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('kim_grib_xy_txt1', params, use_sample=use_sample)) - def kim_grib_xz_txt1( + async def kim_grib_xz_txt1( self, *, use_sample: bool = False, @@ -10958,9 +10923,9 @@ def kim_grib_xz_txt1( Path: /api/typ06/cgi-bin/url/nph-kim_grib_xz_txt1 파라미터: group, nwp, data, varn, lvl_lst, tmfc, hf, lon1, lat1, lon2, lat2, disp""" - return self.call_endpoint('kim_grib_xz_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('kim_grib_xz_txt1', params, use_sample=use_sample)) - def kim_grib_xz_txt1_2( + async def kim_grib_xz_txt1_2( self, *, use_sample: bool = False, @@ -10972,9 +10937,9 @@ def kim_grib_xz_txt1_2( kim_grib_xz_txt1 파라미터: group, nwp, data, varn, lvl_lst, tmfc, hf, map, lon1, lat1, lon2, lat2, disp""" - return self.call_endpoint('kim_grib_xz_txt1_2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_grib_xz_txt1_2', params, use_sample=use_sample)) - def kim_grib_pt_txt1( + async def kim_grib_pt_txt1( self, *, use_sample: bool = False, @@ -10985,9 +10950,9 @@ def kim_grib_pt_txt1( Path: /api/typ06/cgi-bin/url/nph- kim_grib_pt_txt1 파라미터: group, nwp, data, varn, tmfc, hf, X, Y, disp, help""" - return self.call_endpoint('kim_grib_pt_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('kim_grib_pt_txt1', params, use_sample=use_sample)) - def kim_grib_pt_txt1_2( + async def kim_grib_pt_txt1_2( self, *, use_sample: bool = False, @@ -10998,9 +10963,9 @@ def kim_grib_pt_txt1_2( Path: /api/typ06/cgi-bin/url/nph- kim_grib_pt_txt1 파라미터: group, nwp, data, varn, tmfc, hf, lon, lat, level, help""" - return self.call_endpoint('kim_grib_pt_txt1_2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_grib_pt_txt1_2', params, use_sample=use_sample)) - def kim_grib_pt_tmfc( + async def kim_grib_pt_tmfc( self, *, use_sample: bool = False, @@ -11012,9 +10977,9 @@ def kim_grib_pt_tmfc( /api/typ06/url/kim_grib_pt_tmfc.php 파라미터: group, nwp, data, varn, tmfc, ef, X, Y, level, help""" - return self.call_endpoint('kim_grib_pt_tmfc', params, use_sample=use_sample) + return (await self.call_endpoint('kim_grib_pt_tmfc', params, use_sample=use_sample)) - def kim_grib_pt_tmef( + async def kim_grib_pt_tmef( self, *, use_sample: bool = False, @@ -11026,9 +10991,9 @@ def kim_grib_pt_tmef( /api/typ06/url/kim_grib_pt_tmef.php 파라미터: group, nwp, data, varn, tmef, lon, lat, level, help""" - return self.call_endpoint('kim_grib_pt_tmef', params, use_sample=use_sample) + return (await self.call_endpoint('kim_grib_pt_tmef', params, use_sample=use_sample)) - def kim_model_info_service_get_kim_ldaps_unis_all( + async def kim_model_info_service_get_kim_ldaps_unis_all( self, *, use_sample: bool = False, @@ -11040,9 +11005,9 @@ def kim_model_info_service_get_kim_ldaps_unis_all( /api/typ02/openApi/KIMModelInfoService/getKIMLdapsUnisAll 파라미터: baseTime, leadHour, dataTypeCd, dataType""" - return self.call_endpoint('kim_model_info_service_get_kim_ldaps_unis_all', params, use_sample=use_sample) + return (await self.call_endpoint('kim_model_info_service_get_kim_ldaps_unis_all', params, use_sample=use_sample)) - def kim_model_info_service_get_kim_rdaps_unis_all( + async def kim_model_info_service_get_kim_rdaps_unis_all( self, *, use_sample: bool = False, @@ -11054,9 +11019,9 @@ def kim_model_info_service_get_kim_rdaps_unis_all( /api/typ02/openApi/KIMModelInfoService/getKIMRdapsUnisAll 파라미터: baseTime, leadHour, dataTypeCd, dataType""" - return self.call_endpoint('kim_model_info_service_get_kim_rdaps_unis_all', params, use_sample=use_sample) + return (await self.call_endpoint('kim_model_info_service_get_kim_rdaps_unis_all', params, use_sample=use_sample)) - def kim_model_info_service_get_kim_ldaps_unis_area( + async def kim_model_info_service_get_kim_ldaps_unis_area( self, *, use_sample: bool = False, @@ -11068,9 +11033,9 @@ def kim_model_info_service_get_kim_ldaps_unis_area( /api/typ02/openApi/KIMModelInfoService/getKIMLdapsUnisArea 파라미터: baseTime, dataTypeCd, dataType, dongCode""" - return self.call_endpoint('kim_model_info_service_get_kim_ldaps_unis_area', params, use_sample=use_sample) + return (await self.call_endpoint('kim_model_info_service_get_kim_ldaps_unis_area', params, use_sample=use_sample)) - def kim_model_info_service_get_kim_rdaps_unis_area( + async def kim_model_info_service_get_kim_rdaps_unis_area( self, *, use_sample: bool = False, @@ -11082,9 +11047,9 @@ def kim_model_info_service_get_kim_rdaps_unis_area( /api/typ02/openApi/KIMModelInfoService/getKIMRdapsUnisArea 파라미터: baseTime, dataTypeCd, dataType, dongCode""" - return self.call_endpoint('kim_model_info_service_get_kim_rdaps_unis_area', params, use_sample=use_sample) + return (await self.call_endpoint('kim_model_info_service_get_kim_rdaps_unis_area', params, use_sample=use_sample)) - def kim_nc_xy_txt1( + async def kim_nc_xy_txt1( self, *, use_sample: bool = False, @@ -11096,9 +11061,9 @@ def kim_nc_xy_txt1( bin/url/nph-kim_nc_xy_txt1 파라미터: group, nwp, data, name, map, tmfc, hf, disp, help, level""" - return self.call_endpoint('kim_nc_xy_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_xy_txt1', params, use_sample=use_sample)) - def kim_nc_xy_txt1_2( + async def kim_nc_xy_txt1_2( self, *, use_sample: bool = False, @@ -11110,9 +11075,9 @@ def kim_nc_xy_txt1_2( bin/url/nph-kim_nc_xy_txt1 파라미터: group, nwp, data, name, map, sub, sm, tmfc, hf, disp, help, level""" - return self.call_endpoint('kim_nc_xy_txt1_2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_xy_txt1_2', params, use_sample=use_sample)) - def kim_nc_pt_txt1( + async def kim_nc_pt_txt1( self, *, use_sample: bool = False, @@ -11123,9 +11088,9 @@ def kim_nc_pt_txt1( Path: /api/typ06/cgi- bin/url/nph-kim_nc_pt_txt1 파라미터: group, nwp, data, name, tmfc, hf, disp, help, X, Y""" - return self.call_endpoint('kim_nc_pt_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_pt_txt1', params, use_sample=use_sample)) - def kim_nc_pt_txt1_2( + async def kim_nc_pt_txt1_2( self, *, use_sample: bool = False, @@ -11136,9 +11101,9 @@ def kim_nc_pt_txt1_2( Path: /api/typ06/cgi- bin/url/nph-kim_nc_pt_txt1 파라미터: group, nwp, data, name, tmfc, hf, disp, help, lat, lon""" - return self.call_endpoint('kim_nc_pt_txt1_2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_pt_txt1_2', params, use_sample=use_sample)) - def kim_nc_xy_txt2( + async def kim_nc_xy_txt2( self, *, use_sample: bool = False, @@ -11150,9 +11115,9 @@ def kim_nc_xy_txt2( bin/url/nph-kim_nc_xy_txt2 파라미터: group, nwp, data, name, map, tmfc, hf, disp, help, level""" - return self.call_endpoint('kim_nc_xy_txt2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_xy_txt2', params, use_sample=use_sample)) - def kim_nc_xy_txt2_2( + async def kim_nc_xy_txt2_2( self, *, use_sample: bool = False, @@ -11164,9 +11129,9 @@ def kim_nc_xy_txt2_2( bin/url/nph-kim_nc_xy_txt2 파라미터: group, nwp, data, name, map, sub, sm, tmfc, hf, disp, help, level""" - return self.call_endpoint('kim_nc_xy_txt2_2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_xy_txt2_2', params, use_sample=use_sample)) - def kim_nc_pt_txt2( + async def kim_nc_pt_txt2( self, *, use_sample: bool = False, @@ -11177,9 +11142,9 @@ def kim_nc_pt_txt2( Path: /api/typ01/cgi- bin/url/nph-kim_nc_pt_txt2 파라미터: group, nwp, data, name, tmfc, hf, disp, help, X, Y""" - return self.call_endpoint('kim_nc_pt_txt2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_pt_txt2', params, use_sample=use_sample)) - def kim_nc_pt_txt2_2( + async def kim_nc_pt_txt2_2( self, *, use_sample: bool = False, @@ -11190,9 +11155,9 @@ def kim_nc_pt_txt2_2( Path: /api/typ01/cgi- bin/url/nph-kim_nc_pt_txt2 파라미터: group, nwp, data, name, tmfc, hf, disp, help, lat, lon""" - return self.call_endpoint('kim_nc_pt_txt2_2', params, use_sample=use_sample) + return (await self.call_endpoint('kim_nc_pt_txt2_2', params, use_sample=use_sample)) - def marine_large_zone( + async def marine_large_zone( self, *, use_sample: bool = False, @@ -11203,9 +11168,9 @@ def marine_large_zone( Path: /api/typ06/url/marine_large_zone.php 파라미터: tma_fc, tma_ef, Lzone, help, disp""" - return self.call_endpoint('marine_large_zone', params, use_sample=use_sample) + return (await self.call_endpoint('marine_large_zone', params, use_sample=use_sample)) - def marine_small_zone( + async def marine_small_zone( self, *, use_sample: bool = False, @@ -11216,9 +11181,9 @@ def marine_small_zone( Path: /api/typ06/url/marine_small_zone.php 파라미터: tma_fc, tma_ef, Lzone, Szone, disp, help""" - return self.call_endpoint('marine_small_zone', params, use_sample=use_sample) + return (await self.call_endpoint('marine_small_zone', params, use_sample=use_sample)) - def nwp_latlon_api( + async def nwp_latlon_api( self, *, use_sample: bool = False, @@ -11229,9 +11194,9 @@ def nwp_latlon_api( Path: /api/typ01/cgi-bin/url/nph- nwp_latlon_api 파라미터: nwp, latlon, disp""" - return self.call_endpoint('nwp_latlon_api', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_latlon_api', params, use_sample=use_sample)) - def nwp_latlon_file_down( + async def nwp_latlon_file_down( self, *, use_sample: bool = False, @@ -11242,9 +11207,9 @@ def nwp_latlon_file_down( Path: /api/typ01/url/nwp_latlon_file_down.php 파라미터: nwp""" - return self.call_endpoint('nwp_latlon_file_down', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_latlon_file_down', params, use_sample=use_sample)) - def nwp_header( + async def nwp_header( self, *, use_sample: bool = False, @@ -11255,9 +11220,9 @@ def nwp_header( Path: /api/typ06/cgi-bin/url/nph-nwp_header 파라미터: model, nwp, sub, tmfc, ef, help""" - return self.call_endpoint('nwp_header', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_header', params, use_sample=use_sample)) - def um_grib_xy_txt1( + async def um_grib_xy_txt1( self, *, use_sample: bool = False, @@ -11268,9 +11233,9 @@ def um_grib_xy_txt1( Path: /api/typ06/cgi-bin/url/nph- um_grib_xy_txt1 파라미터: group, nwp, data, varn, level, tmfc, hf, disp""" - return self.call_endpoint('um_grib_xy_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_xy_txt1', params, use_sample=use_sample)) - def um_grib_xy_txt1_2( + async def um_grib_xy_txt1_2( self, *, use_sample: bool = False, @@ -11281,9 +11246,9 @@ def um_grib_xy_txt1_2( Path: /api/typ06/cgi-bin/url/nph- um_grib_xy_txt1 파라미터: group, nwp, data, varn, level, map, sub, sm, tmfc, hf, disp""" - return self.call_endpoint('um_grib_xy_txt1_2', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_xy_txt1_2', params, use_sample=use_sample)) - def um_grib_xy_txt1_3( + async def um_grib_xy_txt1_3( self, *, use_sample: bool = False, @@ -11294,9 +11259,9 @@ def um_grib_xy_txt1_3( Path: /api/typ06/cgi-bin/url/nph- um_grib_xy_txt1 파라미터: group, nwp, data, varn, level, map, sm, tmfc, hf, disp""" - return self.call_endpoint('um_grib_xy_txt1_3', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_xy_txt1_3', params, use_sample=use_sample)) - def um_grib_xz_txt1( + async def um_grib_xz_txt1( self, *, use_sample: bool = False, @@ -11308,9 +11273,9 @@ def um_grib_xz_txt1( um_grib_xz_txt1 파라미터: group, nwp, data, varn, lvl_lst, map, tmfc, hf, lon1, lat1, lon2, lat2, disp""" - return self.call_endpoint('um_grib_xz_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_xz_txt1', params, use_sample=use_sample)) - def um_grib_pt_txt1( + async def um_grib_pt_txt1( self, *, use_sample: bool = False, @@ -11321,9 +11286,9 @@ def um_grib_pt_txt1( Path: /api/typ06/cgi-bin/url/nph- um_grib_pt_txt1 파라미터: group, nwp, data, varn, tmfc, hf, X, Y, disp, help""" - return self.call_endpoint('um_grib_pt_txt1', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_pt_txt1', params, use_sample=use_sample)) - def um_grib_pt_txt1_2( + async def um_grib_pt_txt1_2( self, *, use_sample: bool = False, @@ -11335,9 +11300,9 @@ def um_grib_pt_txt1_2( bin/url/nph-um_grib_pt_txt1 파라미터: group, nwp, data, varn, tmfc, hf, level, X, Y, disp, help""" - return self.call_endpoint('um_grib_pt_txt1_2', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_pt_txt1_2', params, use_sample=use_sample)) - def um_grib_pt_txt1_3( + async def um_grib_pt_txt1_3( self, *, use_sample: bool = False, @@ -11348,9 +11313,9 @@ def um_grib_pt_txt1_3( Path: /api/typ06/cgi-bin/url/nph- um_grib_pt_txt1 파라미터: group, nwp, data, varn, tmfc, hf, lon, lat, disp, help""" - return self.call_endpoint('um_grib_pt_txt1_3', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_pt_txt1_3', params, use_sample=use_sample)) - def um_grib_pt_tmfc( + async def um_grib_pt_tmfc( self, *, use_sample: bool = False, @@ -11362,9 +11327,9 @@ def um_grib_pt_tmfc( /api/typ06/url/um_grib_pt_tmfc.php 파라미터: group, nwp, data, varn, tmfc, ef, X, Y, level, help""" - return self.call_endpoint('um_grib_pt_tmfc', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_pt_tmfc', params, use_sample=use_sample)) - def um_grib_pt_tmef( + async def um_grib_pt_tmef( self, *, use_sample: bool = False, @@ -11376,9 +11341,9 @@ def um_grib_pt_tmef( /api/typ06/url/um_grib_pt_tmef.php 파라미터: group, nwp, data, varn, tmef, lon, lat, level, help""" - return self.call_endpoint('um_grib_pt_tmef', params, use_sample=use_sample) + return (await self.call_endpoint('um_grib_pt_tmef', params, use_sample=use_sample)) - def nwp_grib_down( + async def nwp_grib_down( self, *, use_sample: bool = False, @@ -11389,9 +11354,9 @@ def nwp_grib_down( Path: /api/typ06/url/nwp_grib_down.php 파라미터: group, nwp, data, varn, level, tmfc, hf""" - return self.call_endpoint('nwp_grib_down', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_grib_down', params, use_sample=use_sample)) - def nwp_model_info_service_get_ldaps_unis_all( + async def nwp_model_info_service_get_ldaps_unis_all( self, *, use_sample: bool = False, @@ -11403,9 +11368,9 @@ def nwp_model_info_service_get_ldaps_unis_all( /api/typ02/openApi/NwpModelInfoService/getLdapsUnisAll 파라미터: pageNo, numOfRows, dataType, baseTime, leadHour, dataTypeCd""" - return self.call_endpoint('nwp_model_info_service_get_ldaps_unis_all', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_model_info_service_get_ldaps_unis_all', params, use_sample=use_sample)) - def nwp_model_info_service_get_ldaps_unis_area( + async def nwp_model_info_service_get_ldaps_unis_area( self, *, use_sample: bool = False, @@ -11417,9 +11382,9 @@ def nwp_model_info_service_get_ldaps_unis_area( /api/typ02/openApi/NwpModelInfoService/getLdapsUnisArea 파라미터: pageNo, numOfRows, dataType, baseTime, dongCode, dataTypeCd""" - return self.call_endpoint('nwp_model_info_service_get_ldaps_unis_area', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_model_info_service_get_ldaps_unis_area', params, use_sample=use_sample)) - def nwp_model_info_service_get_rdaps_unis_all( + async def nwp_model_info_service_get_rdaps_unis_all( self, *, use_sample: bool = False, @@ -11431,9 +11396,9 @@ def nwp_model_info_service_get_rdaps_unis_all( /api/typ02/openApi/NwpModelInfoService/getRdapsUnisAll 파라미터: pageNo, numOfRows, dataType, baseTime, leadHour, dataTypeCd, dongCode""" - return self.call_endpoint('nwp_model_info_service_get_rdaps_unis_all', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_model_info_service_get_rdaps_unis_all', params, use_sample=use_sample)) - def nwp_model_info_service_get_rdaps_unis_area( + async def nwp_model_info_service_get_rdaps_unis_area( self, *, use_sample: bool = False, @@ -11445,9 +11410,9 @@ def nwp_model_info_service_get_rdaps_unis_area( /api/typ02/openApi/NwpModelInfoService/getRdapsUnisArea 파라미터: pageNo, numOfRows, dataType, baseTime, dongCode, dataTypeCd""" - return self.call_endpoint('nwp_model_info_service_get_rdaps_unis_area', params, use_sample=use_sample) + return (await self.call_endpoint('nwp_model_info_service_get_rdaps_unis_area', params, use_sample=use_sample)) - def dfs_nph_qpf_ana_img( + async def dfs_nph_qpf_ana_img( self, *, use_sample: bool = False, @@ -11458,9 +11423,9 @@ def dfs_nph_qpf_ana_img( Path: /api/typ03/cgi/dfs/nph-qpf_ana_img 파라미터: eva, tm, qpf, ef, map, grid, legend, size, zoom_level, zoom_x, zoom_y, stn, x1, y1""" - return self.call_endpoint('dfs_nph_qpf_ana_img', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_nph_qpf_ana_img', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_recreate_img_url( + async def api_iwa_img_url_api_ret_recreate_img_url( self, *, use_sample: bool = False, @@ -11472,9 +11437,9 @@ def api_iwa_img_url_api_ret_recreate_img_url( /api/typ07/afsiwa/iwa/api/iwaImgUrlApi/retRecreateImgUrl.kfrm 파라미터: analTime, isTyp, imageType, groupName, meta""" - return self.call_endpoint('api_iwa_img_url_api_ret_recreate_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_recreate_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_composite2_img_url( + async def api_iwa_img_url_api_ret_composite2_img_url( self, *, use_sample: bool = False, @@ -11485,9 +11450,9 @@ def api_iwa_img_url_api_ret_composite2_img_url( Path: /api/typ07/afsiwa/iwa/api/iwaImgUrlApi/retComposite2ImgUrl.kfrm 파라미터: analTime, foreTime""" - return self.call_endpoint('api_iwa_img_url_api_ret_composite2_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_composite2_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_composite1_img_url( + async def api_iwa_img_url_api_ret_composite1_img_url( self, *, use_sample: bool = False, @@ -11498,9 +11463,9 @@ def api_iwa_img_url_api_ret_composite1_img_url( Path: /api/typ07/afsiwa/iwa/api/iwaImgUrlApi/retComposite1ImgUrl.kfrm 파라미터: analTime, foreTime""" - return self.call_endpoint('api_iwa_img_url_api_ret_composite1_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_composite1_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_model_img_url( + async def api_iwa_img_url_api_ret_model_img_url( self, *, use_sample: bool = False, @@ -11513,9 +11478,9 @@ def api_iwa_img_url_api_ret_model_img_url( 파라미터: modl, varGrp, var, lev, analTime, foreTime, PROJ, mapRange, ZOOMLVL, stLon, stLat, edLon, edLat, basicSmtLvl, basicTotSmtLvl, repDispCd, symblDispType, isRasterFillCheck, meta, symbl""" - return self.call_endpoint('api_iwa_img_url_api_ret_model_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_model_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_fore_img_url( + async def api_iwa_img_url_api_ret_fore_img_url( self, *, use_sample: bool = False, @@ -11528,9 +11493,9 @@ def api_iwa_img_url_api_ret_fore_img_url( 파라미터: varGrp, var, modl, lev, analTime, foreTime, PROJ, ZOOMLVL, stLon, stLat, edLon, edLat, basicSmtLvl, basicTotSmtLvl, repDispCd, symblDispType, isRasterFillCheck""" - return self.call_endpoint('api_iwa_img_url_api_ret_fore_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_fore_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_ens_img_url( + async def api_iwa_img_url_api_ret_ens_img_url( self, *, use_sample: bool = False, @@ -11543,9 +11508,9 @@ def api_iwa_img_url_api_ret_ens_img_url( 파라미터: modl, ensType, varGrp, var, mem, lev, analTime, foreTime, PROJ, ZOOMLVL, stLon, stLat, edLon, edLat, basicTotSmtLvl, repDispCd, symblDispType, isRasterFillCheck, meta, symbl""" - return self.call_endpoint('api_iwa_img_url_api_ret_ens_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_ens_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_ocean_img_url( + async def api_iwa_img_url_api_ret_ocean_img_url( self, *, use_sample: bool = False, @@ -11558,9 +11523,9 @@ def api_iwa_img_url_api_ret_ocean_img_url( 파라미터: modlGrp, modl, var, mem, lev, analTime, foreTime, PROJ, ZOOMLVL, stLon, stLat, edLon, edLat, basicTotSmtLvl, repDispCd, symblDispType, isRasterFillCheck, meta, symbl""" - return self.call_endpoint('api_iwa_img_url_api_ret_ocean_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_ocean_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_crss_sctn_img_url( + async def api_iwa_img_url_api_ret_crss_sctn_img_url( self, *, use_sample: bool = False, @@ -11573,9 +11538,9 @@ def api_iwa_img_url_api_ret_crss_sctn_img_url( 파라미터: modelCd, variable, isFill3, analTime, foreTime, locationLon01, locationLat01, locationLon02, locationLat02, minPresAlt, maxPresAlt, log, width, height, layerInfo""" - return self.call_endpoint('api_iwa_img_url_api_ret_crss_sctn_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_crss_sctn_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_back_map_url( + async def api_iwa_img_url_api_ret_back_map_url( self, *, use_sample: bool = False, @@ -11587,9 +11552,9 @@ def api_iwa_img_url_api_ret_back_map_url( /api/typ07/afsiwa/iwa/api/iwaImgUrlApi/retBackMapUrl.kfrm 파라미터: type, projection, ZOOMLVL, stLon, stLat, edLon, edLat, meta""" - return self.call_endpoint('api_iwa_img_url_api_ret_back_map_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_back_map_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_obs_img_url( + async def api_iwa_img_url_api_ret_obs_img_url( self, *, use_sample: bool = False, @@ -11602,9 +11567,9 @@ def api_iwa_img_url_api_ret_obs_img_url( 파라미터: obs, varGrp, var, lev, analTime, PROJ, ZOOMLVL, stLon, stLat, edLon, edLat, basicSmtLvl, basicTotSmtLvl, repDispCd, symblDispType, meta""" - return self.call_endpoint('api_iwa_img_url_api_ret_obs_img_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_obs_img_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_mdl_sample_data_url( + async def api_iwa_img_url_api_ret_mdl_sample_data_url( self, *, use_sample: bool = False, @@ -11617,9 +11582,9 @@ def api_iwa_img_url_api_ret_mdl_sample_data_url( 파라미터: menuGrpCd, menu01, menu02, menu03, varListCd, vrtcLayrCd, analTime, foreTime, basicSmtLvl, location, project, meta""" - return self.call_endpoint('api_iwa_img_url_api_ret_mdl_sample_data_url', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_mdl_sample_data_url', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_model_img_url_2( + async def api_iwa_img_url_api_ret_model_img_url_2( self, *, use_sample: bool = False, @@ -11631,9 +11596,9 @@ def api_iwa_img_url_api_ret_model_img_url_2( 파라미터: modl, varGrp, var, lev, analTime, foreTime, PROJ, ZOOMLVL, stLon, stLat, edLon, edLat, basicSmtLvl, basicTotSmtLvl, repDispCd, symblDispType, isRasterFillCheck, meta, symbl""" - return self.call_endpoint('api_iwa_img_url_api_ret_model_img_url_2', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_model_img_url_2', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_back_map_url_2( + async def api_iwa_img_url_api_ret_back_map_url_2( self, *, use_sample: bool = False, @@ -11644,9 +11609,9 @@ def api_iwa_img_url_api_ret_back_map_url_2( Path: /api/typ07/afsiwa/iwa/api/iwaImgUrlApi/retBackMapUrl.kfrm 파라미터: type, projection, ZOOMLVL, stLon, stLat, edLon, edLat, meta, mdl, basicSmtLvl""" - return self.call_endpoint('api_iwa_img_url_api_ret_back_map_url_2', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_back_map_url_2', params, use_sample=use_sample)) - def api_iwa_img_url_api_ret_grid_img( + async def api_iwa_img_url_api_ret_grid_img( self, *, use_sample: bool = False, @@ -11658,9 +11623,9 @@ def api_iwa_img_url_api_ret_grid_img( 파라미터: PROJ, ZOOMLVL, stLon, stLat, edLon, edLat, contourLineColor, contourLineDiv, contourLineThck, meta, mdl, basicSmtLvl""" - return self.call_endpoint('api_iwa_img_url_api_ret_grid_img', params, use_sample=use_sample) + return (await self.call_endpoint('api_iwa_img_url_api_ret_grid_img', params, use_sample=use_sample)) - def wthr_chart_info_service_get_auxillary_chart( + async def wthr_chart_info_service_get_auxillary_chart( self, *, use_sample: bool = False, @@ -11670,9 +11635,9 @@ def wthr_chart_info_service_get_auxillary_chart( Path: /api/typ02/openApi/WthrChartInfoService/getAuxillaryChart 파라미터: pageNo, numOfRows, dataType, code1, code2, time""" - return self.call_endpoint('wthr_chart_info_service_get_auxillary_chart', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_chart_info_service_get_auxillary_chart', params, use_sample=use_sample)) - def wthr_chart_info_service_get_surface_chart( + async def wthr_chart_info_service_get_surface_chart( self, *, use_sample: bool = False, @@ -11682,9 +11647,9 @@ def wthr_chart_info_service_get_surface_chart( Path: /api/typ02/openApi/WthrChartInfoService/getSurfaceChart 파라미터: pageNo, numOfRows, dataType, code, time""" - return self.call_endpoint('wthr_chart_info_service_get_surface_chart', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_chart_info_service_get_surface_chart', params, use_sample=use_sample)) - def fct_shrt_reg( + async def fct_shrt_reg( self, *, use_sample: bool = False, @@ -11695,9 +11660,9 @@ def fct_shrt_reg( Path: /api/typ01/url/fct_shrt_reg.php 파라미터: tmfc""" - return self.call_endpoint('fct_shrt_reg', params, use_sample=use_sample) + return (await self.call_endpoint('fct_shrt_reg', params, use_sample=use_sample)) - def fct_afs_ds( + async def fct_afs_ds( self, *, use_sample: bool = False, @@ -11707,9 +11672,9 @@ def fct_afs_ds( Path: /api/typ01/url/fct_afs_ds.php 파라미터: stn, tmfc1, tmfc2, disp, help""" - return self.call_endpoint('fct_afs_ds', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_ds', params, use_sample=use_sample)) - def fct_afs_dl( + async def fct_afs_dl( self, *, use_sample: bool = False, @@ -11720,9 +11685,9 @@ def fct_afs_dl( Path: /api/typ01/url/fct_afs_dl.php 파라미터: reg, tmfc1, tmfc2, disp, help""" - return self.call_endpoint('fct_afs_dl', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_dl', params, use_sample=use_sample)) - def fct_afs_dl2( + async def fct_afs_dl2( self, *, use_sample: bool = False, @@ -11733,9 +11698,9 @@ def fct_afs_dl2( Path: /api/typ01/url/fct_afs_dl2.php 파라미터: reg, tmfc1, tmfc2, disp, help""" - return self.call_endpoint('fct_afs_dl2', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_dl2', params, use_sample=use_sample)) - def fct_afs_do( + async def fct_afs_do( self, *, use_sample: bool = False, @@ -11746,9 +11711,9 @@ def fct_afs_do( Path: /api/typ01/url/fct_afs_do.php 파라미터: reg, tmfc1, tmfc2, disp, help""" - return self.call_endpoint('fct_afs_do', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_do', params, use_sample=use_sample)) - def dfs_shrt_grd( + async def dfs_shrt_grd( self, *, use_sample: bool = False, @@ -11758,9 +11723,9 @@ def dfs_shrt_grd( Path: /api/typ01/cgi-bin/url/nph-dfs_shrt_grd 파라미터: tmfc, tmef, vars""" - return self.call_endpoint('dfs_shrt_grd', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_shrt_grd', params, use_sample=use_sample)) - def dfs_vsrt_grd( + async def dfs_vsrt_grd( self, *, use_sample: bool = False, @@ -11770,9 +11735,9 @@ def dfs_vsrt_grd( Path: /api/typ01/cgi-bin/url/nph-dfs_vsrt_grd 파라미터: tmfc, tmef, vars""" - return self.call_endpoint('dfs_vsrt_grd', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_vsrt_grd', params, use_sample=use_sample)) - def dfs_odam_grd( + async def dfs_odam_grd( self, *, use_sample: bool = False, @@ -11782,9 +11747,9 @@ def dfs_odam_grd( Path: /api/typ01/cgi-bin/url/nph-dfs_odam_grd 파라미터: tmfc, vars""" - return self.call_endpoint('dfs_odam_grd', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_odam_grd', params, use_sample=use_sample)) - def dfs_xy_lonlat( + async def dfs_xy_lonlat( self, *, use_sample: bool = False, @@ -11795,9 +11760,9 @@ def dfs_xy_lonlat( Path: /api/typ01/cgi- bin/url/nph-dfs_xy_lonlat 파라미터: x, y, help""" - return self.call_endpoint('dfs_xy_lonlat', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_xy_lonlat', params, use_sample=use_sample)) - def dfs_xy_lonlat_2( + async def dfs_xy_lonlat_2( self, *, use_sample: bool = False, @@ -11808,9 +11773,9 @@ def dfs_xy_lonlat_2( Path: /api/typ01/cgi- bin/url/nph-dfs_xy_lonlat 파라미터: lon, lat, help""" - return self.call_endpoint('dfs_xy_lonlat_2', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_xy_lonlat_2', params, use_sample=use_sample)) - def vilage_fcst_msg_service_get_wthr_situation( + async def vilage_fcst_msg_service_get_wthr_situation( self, *, use_sample: bool = False, @@ -11822,9 +11787,9 @@ def vilage_fcst_msg_service_get_wthr_situation( /api/typ02/openApi/VilageFcstMsgService/getWthrSituation 파라미터: pageNo, numOfRows, dataType, stnId""" - return self.call_endpoint('vilage_fcst_msg_service_get_wthr_situation', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_msg_service_get_wthr_situation', params, use_sample=use_sample)) - def vilage_fcst_msg_service_get_land_fcst( + async def vilage_fcst_msg_service_get_land_fcst( self, *, use_sample: bool = False, @@ -11834,9 +11799,9 @@ def vilage_fcst_msg_service_get_land_fcst( Path: /api/typ02/openApi/VilageFcstMsgService/getLandFcst 파라미터: pageNo, numOfRows, dataType, regId""" - return self.call_endpoint('vilage_fcst_msg_service_get_land_fcst', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_msg_service_get_land_fcst', params, use_sample=use_sample)) - def vilage_fcst_msg_service_get_land_fcst_2( + async def vilage_fcst_msg_service_get_land_fcst_2( self, *, use_sample: bool = False, @@ -11846,9 +11811,9 @@ def vilage_fcst_msg_service_get_land_fcst_2( Path: /api/typ02/openApi/VilageFcstMsgService/getLandFcst 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('vilage_fcst_msg_service_get_land_fcst_2', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_msg_service_get_land_fcst_2', params, use_sample=use_sample)) - def vilage_fcst_msg_service_get_sea_fcst( + async def vilage_fcst_msg_service_get_sea_fcst( self, *, use_sample: bool = False, @@ -11858,9 +11823,9 @@ def vilage_fcst_msg_service_get_sea_fcst( Path: /api/typ02/openApi/VilageFcstMsgService/getSeaFcst 파라미터: pageNo, numOfRows, dataType, regId""" - return self.call_endpoint('vilage_fcst_msg_service_get_sea_fcst', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_msg_service_get_sea_fcst', params, use_sample=use_sample)) - def vilage_fcst_msg_service_get_sea_fcst_2( + async def vilage_fcst_msg_service_get_sea_fcst_2( self, *, use_sample: bool = False, @@ -11870,9 +11835,9 @@ def vilage_fcst_msg_service_get_sea_fcst_2( Path: /api/typ02/openApi/VilageFcstMsgService/getSeaFcst 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('vilage_fcst_msg_service_get_sea_fcst_2', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_msg_service_get_sea_fcst_2', params, use_sample=use_sample)) - def vilage_fcst_info_service_2_0_get_ultra_srt_ncst( + async def vilage_fcst_info_service_2_0_get_ultra_srt_ncst( self, *, use_sample: bool = False, @@ -11884,9 +11849,9 @@ def vilage_fcst_info_service_2_0_get_ultra_srt_ncst( /api/typ02/openApi/VilageFcstInfoService_2.0/getUltraSrtNcst 파라미터: pageNo, numOfRows, dataType, base_date, base_time, nx, ny""" - return self.call_endpoint('vilage_fcst_info_service_2_0_get_ultra_srt_ncst', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_info_service_2_0_get_ultra_srt_ncst', params, use_sample=use_sample)) - def vilage_fcst_info_service_2_0_get_ultra_srt_fcst( + async def vilage_fcst_info_service_2_0_get_ultra_srt_fcst( self, *, use_sample: bool = False, @@ -11898,9 +11863,9 @@ def vilage_fcst_info_service_2_0_get_ultra_srt_fcst( /api/typ02/openApi/VilageFcstInfoService_2.0/getUltraSrtFcst 파라미터: pageNo, numOfRows, dataType, base_date, base_time, nx, ny""" - return self.call_endpoint('vilage_fcst_info_service_2_0_get_ultra_srt_fcst', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_info_service_2_0_get_ultra_srt_fcst', params, use_sample=use_sample)) - def vilage_fcst_info_service_2_0_get_vilage_fcst( + async def vilage_fcst_info_service_2_0_get_vilage_fcst( self, *, use_sample: bool = False, @@ -11912,9 +11877,9 @@ def vilage_fcst_info_service_2_0_get_vilage_fcst( /api/typ02/openApi/VilageFcstInfoService_2.0/getVilageFcst 파라미터: pageNo, numOfRows, dataType, base_date, base_time, nx, ny""" - return self.call_endpoint('vilage_fcst_info_service_2_0_get_vilage_fcst', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_info_service_2_0_get_vilage_fcst', params, use_sample=use_sample)) - def vilage_fcst_info_service_2_0_get_fcst_version( + async def vilage_fcst_info_service_2_0_get_fcst_version( self, *, use_sample: bool = False, @@ -11926,9 +11891,9 @@ def vilage_fcst_info_service_2_0_get_fcst_version( /api/typ02/openApi/VilageFcstInfoService_2.0/getFcstVersion 파라미터: pageNo, numOfRows, dataType, ftype, basedatetime""" - return self.call_endpoint('vilage_fcst_info_service_2_0_get_fcst_version', params, use_sample=use_sample) + return (await self.call_endpoint('vilage_fcst_info_service_2_0_get_fcst_version', params, use_sample=use_sample)) - def dfs_nph_dfs_shrt_ana_5d_test( + async def dfs_nph_dfs_shrt_ana_5d_test( self, *, use_sample: bool = False, @@ -11940,9 +11905,9 @@ def dfs_nph_dfs_shrt_ana_5d_test( 파라미터: data0, data1, tm_ef, tm_fc, dtm, map, mask, color, size, effect, overlay, zoom_rate, zoom_level, zoom_x, zoom_y, auto_man, mode, interval, rand""" - return self.call_endpoint('dfs_nph_dfs_shrt_ana_5d_test', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_nph_dfs_shrt_ana_5d_test', params, use_sample=use_sample)) - def dfs_nph_dfs_vsrt_ana2( + async def dfs_nph_dfs_vsrt_ana2( self, *, use_sample: bool = False, @@ -11954,9 +11919,9 @@ def dfs_nph_dfs_vsrt_ana2( 파라미터: data0, tm_fc, data1, tm_ef, dtm, map, mask, color, size, effect, overlay, zoom_rate, zoom_level, zoom_x, zoom_y, auto_man, mode, rand""" - return self.call_endpoint('dfs_nph_dfs_vsrt_ana2', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_nph_dfs_vsrt_ana2', params, use_sample=use_sample)) - def dfs_latlon_api( + async def dfs_latlon_api( self, *, use_sample: bool = False, @@ -11967,9 +11932,9 @@ def dfs_latlon_api( Path: /api/typ01/cgi-bin/url/nph- dfs_latlon_api 파라미터: fct, latlon, disp""" - return self.call_endpoint('dfs_latlon_api', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_latlon_api', params, use_sample=use_sample)) - def dfs_latlon_file_down( + async def dfs_latlon_file_down( self, *, use_sample: bool = False, @@ -11980,9 +11945,9 @@ def dfs_latlon_file_down( Path: /api/typ01/url/dfs_latlon_file_down.php 파라미터: fct""" - return self.call_endpoint('dfs_latlon_file_down', params, use_sample=use_sample) + return (await self.call_endpoint('dfs_latlon_file_down', params, use_sample=use_sample)) - def fct_medm_reg( + async def fct_medm_reg( self, *, use_sample: bool = False, @@ -11993,9 +11958,9 @@ def fct_medm_reg( Path: /api/typ01/url/fct_medm_reg.php 파라미터: tmfc""" - return self.call_endpoint('fct_medm_reg', params, use_sample=use_sample) + return (await self.call_endpoint('fct_medm_reg', params, use_sample=use_sample)) - def fct_afs_ws( + async def fct_afs_ws( self, *, use_sample: bool = False, @@ -12005,9 +11970,9 @@ def fct_afs_ws( Path: /api/typ01/url/fct_afs_ws.php 파라미터: stn, tmfc1, tmfc2, disp, help""" - return self.call_endpoint('fct_afs_ws', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_ws', params, use_sample=use_sample)) - def fct_afs_wl( + async def fct_afs_wl( self, *, use_sample: bool = False, @@ -12018,9 +11983,9 @@ def fct_afs_wl( Path: /api/typ01/url/fct_afs_wl.php 파라미터: reg, tmfc1, tmfc2, tmef1, tmef2, disp, help""" - return self.call_endpoint('fct_afs_wl', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_wl', params, use_sample=use_sample)) - def fct_afs_wc( + async def fct_afs_wc( self, *, use_sample: bool = False, @@ -12031,9 +11996,9 @@ def fct_afs_wc( Path: /api/typ01/url/fct_afs_wc.php 파라미터: reg, tmfc1, tmfc2, tmef1, tmef2, disp, help""" - return self.call_endpoint('fct_afs_wc', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_wc', params, use_sample=use_sample)) - def fct_afs_wo( + async def fct_afs_wo( self, *, use_sample: bool = False, @@ -12044,9 +12009,9 @@ def fct_afs_wo( Path: /api/typ01/url/fct_afs_wo.php 파라미터: reg, tmfc1, tmfc2, tmef1, tmef2, disp, help""" - return self.call_endpoint('fct_afs_wo', params, use_sample=use_sample) + return (await self.call_endpoint('fct_afs_wo', params, use_sample=use_sample)) - def mid_fcst_info_service_get_mid_sea_fcst( + async def mid_fcst_info_service_get_mid_sea_fcst( self, *, use_sample: bool = False, @@ -12056,9 +12021,9 @@ def mid_fcst_info_service_get_mid_sea_fcst( Path: /api/typ02/openApi/MidFcstInfoService/getMidSeaFcst 파라미터: pageNo, numOfRows, dataType, regId, tmFc""" - return self.call_endpoint('mid_fcst_info_service_get_mid_sea_fcst', params, use_sample=use_sample) + return (await self.call_endpoint('mid_fcst_info_service_get_mid_sea_fcst', params, use_sample=use_sample)) - def mid_fcst_info_service_get_mid_sea_fcst_2( + async def mid_fcst_info_service_get_mid_sea_fcst_2( self, *, use_sample: bool = False, @@ -12068,9 +12033,9 @@ def mid_fcst_info_service_get_mid_sea_fcst_2( Path: /api/typ02/openApi/MidFcstInfoService/getMidSeaFcst 파라미터: pageNo, numOfRows, dataType, tmFc""" - return self.call_endpoint('mid_fcst_info_service_get_mid_sea_fcst_2', params, use_sample=use_sample) + return (await self.call_endpoint('mid_fcst_info_service_get_mid_sea_fcst_2', params, use_sample=use_sample)) - def mid_fcst_info_service_get_mid_ta( + async def mid_fcst_info_service_get_mid_ta( self, *, use_sample: bool = False, @@ -12081,9 +12046,9 @@ def mid_fcst_info_service_get_mid_ta( Path: /api/typ02/openApi/MidFcstInfoService/getMidTa 파라미터: pageNo, numOfRows, dataType, regId, tmFc""" - return self.call_endpoint('mid_fcst_info_service_get_mid_ta', params, use_sample=use_sample) + return (await self.call_endpoint('mid_fcst_info_service_get_mid_ta', params, use_sample=use_sample)) - def mid_fcst_info_service_get_mid_ta_2( + async def mid_fcst_info_service_get_mid_ta_2( self, *, use_sample: bool = False, @@ -12094,9 +12059,9 @@ def mid_fcst_info_service_get_mid_ta_2( Path: /api/typ02/openApi/MidFcstInfoService/getMidTa 파라미터: pageNo, numOfRows, dataType, tmFc""" - return self.call_endpoint('mid_fcst_info_service_get_mid_ta_2', params, use_sample=use_sample) + return (await self.call_endpoint('mid_fcst_info_service_get_mid_ta_2', params, use_sample=use_sample)) - def mid_fcst_info_service_get_mid_land_fcst( + async def mid_fcst_info_service_get_mid_land_fcst( self, *, use_sample: bool = False, @@ -12106,9 +12071,9 @@ def mid_fcst_info_service_get_mid_land_fcst( Path: /api/typ02/openApi/MidFcstInfoService/getMidLandFcst 파라미터: pageNo, numOfRows, dataType, regId, tmFc""" - return self.call_endpoint('mid_fcst_info_service_get_mid_land_fcst', params, use_sample=use_sample) + return (await self.call_endpoint('mid_fcst_info_service_get_mid_land_fcst', params, use_sample=use_sample)) - def mid_fcst_info_service_get_mid_land_fcst_2( + async def mid_fcst_info_service_get_mid_land_fcst_2( self, *, use_sample: bool = False, @@ -12118,9 +12083,9 @@ def mid_fcst_info_service_get_mid_land_fcst_2( Path: /api/typ02/openApi/MidFcstInfoService/getMidLandFcst 파라미터: pageNo, numOfRows, dataType, tmFc""" - return self.call_endpoint('mid_fcst_info_service_get_mid_land_fcst_2', params, use_sample=use_sample) + return (await self.call_endpoint('mid_fcst_info_service_get_mid_land_fcst_2', params, use_sample=use_sample)) - def mid_fcst_info_service_get_mid_fcst( + async def mid_fcst_info_service_get_mid_fcst( self, *, use_sample: bool = False, @@ -12131,9 +12096,9 @@ def mid_fcst_info_service_get_mid_fcst( Path: /api/typ02/openApi/MidFcstInfoService/getMidFcst 파라미터: pageNo, numOfRows, dataType, stnId, tmFc""" - return self.call_endpoint('mid_fcst_info_service_get_mid_fcst', params, use_sample=use_sample) + return (await self.call_endpoint('mid_fcst_info_service_get_mid_fcst', params, use_sample=use_sample)) - def wrn_reg( + async def wrn_reg( self, *, use_sample: bool = False, @@ -12143,9 +12108,9 @@ def wrn_reg( Path: /api/typ01/url/wrn_reg.php 파라미터: tmfc""" - return self.call_endpoint('wrn_reg', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_reg', params, use_sample=use_sample)) - def wrn_met_data( + async def wrn_met_data( self, *, use_sample: bool = False, @@ -12156,9 +12121,9 @@ def wrn_met_data( Path: /api/typ01/url/wrn_met_data.php 파라미터: reg, wrn, tmfc1, tmfc2, disp, help""" - return self.call_endpoint('wrn_met_data', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_met_data', params, use_sample=use_sample)) - def wrn_inf_rpt( + async def wrn_inf_rpt( self, *, use_sample: bool = False, @@ -12169,9 +12134,9 @@ def wrn_inf_rpt( Path: /api/typ01/url/wrn_inf_rpt.php 파라미터: tmfc1, tmfc2, stn, disp, help""" - return self.call_endpoint('wrn_inf_rpt', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_inf_rpt', params, use_sample=use_sample)) - def wthr_cmt_rpt( + async def wthr_cmt_rpt( self, *, use_sample: bool = False, @@ -12182,9 +12147,9 @@ def wthr_cmt_rpt( Path: /api/typ01/url/wthr_cmt_rpt.php 파라미터: tmfc1, tmfc2, stn, subcd, disp, help""" - return self.call_endpoint('wthr_cmt_rpt', params, use_sample=use_sample) + return (await self.call_endpoint('wthr_cmt_rpt', params, use_sample=use_sample)) - def wrn_now_data( + async def wrn_now_data( self, *, use_sample: bool = False, @@ -12194,9 +12159,9 @@ def wrn_now_data( Path: /api/typ01/url/wrn_now_data.php 파라미터: fe, tm, disp, help""" - return self.call_endpoint('wrn_now_data', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_now_data', params, use_sample=use_sample)) - def wrn_now_data_new( + async def wrn_now_data_new( self, *, use_sample: bool = False, @@ -12206,9 +12171,9 @@ def wrn_now_data_new( Path: /api/typ01/url/wrn_now_data_new.php 파라미터: fe, tm, disp, help""" - return self.call_endpoint('wrn_now_data_new', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_now_data_new', params, use_sample=use_sample)) - def wrn_nph_wrn7( + async def wrn_nph_wrn7( self, *, use_sample: bool = False, @@ -12219,9 +12184,9 @@ def wrn_nph_wrn7( Path: /api/typ03/cgi/wrn/nph-wrn7 파라미터: out, tmef, city, name, tm, lon, lat, range, size, wrn""" - return self.call_endpoint('wrn_nph_wrn7', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_nph_wrn7', params, use_sample=use_sample)) - def ifs_fct_pstt( + async def ifs_fct_pstt( self, *, use_sample: bool = False, @@ -12232,9 +12197,9 @@ def ifs_fct_pstt( Path: /api/typ01/url/ifs_fct_pstt.php 파라미터: tmef1, tmef2, ifpar, help""" - return self.call_endpoint('ifs_fct_pstt', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_fct_pstt', params, use_sample=use_sample)) - def ifs_fct_pstt_2( + async def ifs_fct_pstt_2( self, *, use_sample: bool = False, @@ -12245,9 +12210,9 @@ def ifs_fct_pstt_2( Path: /api/typ01/url/ifs_fct_pstt.php 파라미터: tmfc1, tmfc2, ifpar, help""" - return self.call_endpoint('ifs_fct_pstt_2', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_fct_pstt_2', params, use_sample=use_sample)) - def ifs_fct_pstt_3( + async def ifs_fct_pstt_3( self, *, use_sample: bool = False, @@ -12258,9 +12223,9 @@ def ifs_fct_pstt_3( Path: /api/typ01/url/ifs_fct_pstt.php 파라미터: tmef1, tmef2, ifarea, regid, help""" - return self.call_endpoint('ifs_fct_pstt_3', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_fct_pstt_3', params, use_sample=use_sample)) - def ifs_ilvl_zone_cnt( + async def ifs_ilvl_zone_cnt( self, *, use_sample: bool = False, @@ -12271,9 +12236,9 @@ def ifs_ilvl_zone_cnt( Path: /api/typ01/url/ifs_ilvl_zone_cnt.php 파라미터: help, tmfc1, tmfc2""" - return self.call_endpoint('ifs_ilvl_zone_cnt', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_zone_cnt', params, use_sample=use_sample)) - def ifs_ilvl_zone_cnt_2( + async def ifs_ilvl_zone_cnt_2( self, *, use_sample: bool = False, @@ -12284,9 +12249,9 @@ def ifs_ilvl_zone_cnt_2( Path: /api/typ01/url/ifs_ilvl_zone_cnt.php 파라미터: help, tmef1, tmef2""" - return self.call_endpoint('ifs_ilvl_zone_cnt_2', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_zone_cnt_2', params, use_sample=use_sample)) - def ifs_ilvl_zone_cnt_3( + async def ifs_ilvl_zone_cnt_3( self, *, use_sample: bool = False, @@ -12297,9 +12262,9 @@ def ifs_ilvl_zone_cnt_3( Path: /api/typ01/url/ifs_ilvl_zone_cnt.php 파라미터: help, tmef1, tmef2, ifarea, stn""" - return self.call_endpoint('ifs_ilvl_zone_cnt_3', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_zone_cnt_3', params, use_sample=use_sample)) - def ifs_ilvl_zone_cnt_4( + async def ifs_ilvl_zone_cnt_4( self, *, use_sample: bool = False, @@ -12309,9 +12274,9 @@ def ifs_ilvl_zone_cnt_4( Path: /api/typ01/url/ifs_ilvl_zone_cnt.php 파라미터: help, tmef1, tmef2, ilvl""" - return self.call_endpoint('ifs_ilvl_zone_cnt_4', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_zone_cnt_4', params, use_sample=use_sample)) - def ifs_ilvl_dmap( + async def ifs_ilvl_dmap( self, *, use_sample: bool = False, @@ -12321,9 +12286,9 @@ def ifs_ilvl_dmap( Path: /api/typ01/url/ifs_ilvl_dmap.php 파라미터: tmfc""" - return self.call_endpoint('ifs_ilvl_dmap', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_dmap', params, use_sample=use_sample)) - def ifs_ilvl_dmap_2( + async def ifs_ilvl_dmap_2( self, *, use_sample: bool = False, @@ -12334,9 +12299,9 @@ def ifs_ilvl_dmap_2( Path: /api/typ01/url/ifs_ilvl_dmap.php 파라미터: tmfc, stn""" - return self.call_endpoint('ifs_ilvl_dmap_2', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_dmap_2', params, use_sample=use_sample)) - def ifs_ilvl_dmap_3( + async def ifs_ilvl_dmap_3( self, *, use_sample: bool = False, @@ -12347,9 +12312,9 @@ def ifs_ilvl_dmap_3( Path: /api/typ01/url/ifs_ilvl_dmap.php 파라미터: tmfc, ifpar""" - return self.call_endpoint('ifs_ilvl_dmap_3', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_dmap_3', params, use_sample=use_sample)) - def ifs_ilvl_dmap_4( + async def ifs_ilvl_dmap_4( self, *, use_sample: bool = False, @@ -12360,9 +12325,9 @@ def ifs_ilvl_dmap_4( Path: /api/typ01/url/ifs_ilvl_dmap.php 파라미터: tmfc, ifarea""" - return self.call_endpoint('ifs_ilvl_dmap_4', params, use_sample=use_sample) + return (await self.call_endpoint('ifs_ilvl_dmap_4', params, use_sample=use_sample)) - def fcst_zone_info_service_get_fcst_zone_cd( + async def fcst_zone_info_service_get_fcst_zone_cd( self, *, use_sample: bool = False, @@ -12374,9 +12339,9 @@ def fcst_zone_info_service_get_fcst_zone_cd( /api/typ02/openApi/FcstZoneInfoService/getFcstZoneCd 파라미터: pageNo, numOfRows, dataType, regId""" - return self.call_endpoint('fcst_zone_info_service_get_fcst_zone_cd', params, use_sample=use_sample) + return (await self.call_endpoint('fcst_zone_info_service_get_fcst_zone_cd', params, use_sample=use_sample)) - def fcst_zone_info_service_get_fcst_zone_cd_2( + async def fcst_zone_info_service_get_fcst_zone_cd_2( self, *, use_sample: bool = False, @@ -12387,9 +12352,9 @@ def fcst_zone_info_service_get_fcst_zone_cd_2( Path: /api/typ02/openApi/FcstZoneInfoService/getFcstZoneCd 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('fcst_zone_info_service_get_fcst_zone_cd_2', params, use_sample=use_sample) + return (await self.call_endpoint('fcst_zone_info_service_get_fcst_zone_cd_2', params, use_sample=use_sample)) - def wethr_basic_info_service_get_wrn_zone_cd( + async def wethr_basic_info_service_get_wrn_zone_cd( self, *, use_sample: bool = False, @@ -12399,9 +12364,9 @@ def wethr_basic_info_service_get_wrn_zone_cd( Path: /api/typ02/openApi/WethrBasicInfoService/getWrnZoneCd 파라미터: pageNo, numOfRows, dataType, korName""" - return self.call_endpoint('wethr_basic_info_service_get_wrn_zone_cd', params, use_sample=use_sample) + return (await self.call_endpoint('wethr_basic_info_service_get_wrn_zone_cd', params, use_sample=use_sample)) - def wrn_reg_aws( + async def wrn_reg_aws( self, *, use_sample: bool = False, @@ -12412,9 +12377,9 @@ def wrn_reg_aws( Path: /api/typ01/url/wrn_reg_aws.php 파라미터: tm, disp, help""" - return self.call_endpoint('wrn_reg_aws', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_reg_aws', params, use_sample=use_sample)) - def wrn_reg_aws2( + async def wrn_reg_aws2( self, *, use_sample: bool = False, @@ -12425,9 +12390,9 @@ def wrn_reg_aws2( Path: /api/typ01/url/wrn_reg_aws2.php 파라미터: tm, disp, help""" - return self.call_endpoint('wrn_reg_aws2', params, use_sample=use_sample) + return (await self.call_endpoint('wrn_reg_aws2', params, use_sample=use_sample)) - def gts_syn1( + async def gts_syn1( self, *, use_sample: bool = False, @@ -12438,9 +12403,9 @@ def gts_syn1( Path: /api/typ01/url/gts_syn1.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_syn1', params, use_sample=use_sample) + return (await self.call_endpoint('gts_syn1', params, use_sample=use_sample)) - def gts_bufr_syn1( + async def gts_bufr_syn1( self, *, use_sample: bool = False, @@ -12451,9 +12416,9 @@ def gts_bufr_syn1( Path: /api/typ01/url/gts_bufr_syn1.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_bufr_syn1', params, use_sample=use_sample) + return (await self.call_endpoint('gts_bufr_syn1', params, use_sample=use_sample)) - def gts_bufr_syn( + async def gts_bufr_syn( self, *, use_sample: bool = False, @@ -12464,9 +12429,9 @@ def gts_bufr_syn( Path: /api/typ01/url/gts_bufr_syn.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_bufr_syn', params, use_sample=use_sample) + return (await self.call_endpoint('gts_bufr_syn', params, use_sample=use_sample)) - def gts_syn( + async def gts_syn( self, *, use_sample: bool = False, @@ -12477,9 +12442,9 @@ def gts_syn( Path: /api/typ01/url/gts_syn.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_syn', params, use_sample=use_sample) + return (await self.call_endpoint('gts_syn', params, use_sample=use_sample)) - def gts_ship1( + async def gts_ship1( self, *, use_sample: bool = False, @@ -12489,9 +12454,9 @@ def gts_ship1( Path: /api/typ01/url/gts_ship1.php 파라미터: tm, dtm, help""" - return self.call_endpoint('gts_ship1', params, use_sample=use_sample) + return (await self.call_endpoint('gts_ship1', params, use_sample=use_sample)) - def gts_bufr_ship( + async def gts_bufr_ship( self, *, use_sample: bool = False, @@ -12502,9 +12467,9 @@ def gts_bufr_ship( Path: /api/typ01/url/gts_bufr_ship.php 파라미터: tm, dtm, help""" - return self.call_endpoint('gts_bufr_ship', params, use_sample=use_sample) + return (await self.call_endpoint('gts_bufr_ship', params, use_sample=use_sample)) - def gts_ship( + async def gts_ship( self, *, use_sample: bool = False, @@ -12515,9 +12480,9 @@ def gts_ship( Path: /api/typ01/url/gts_ship.php 파라미터: tm, dtm, help""" - return self.call_endpoint('gts_ship', params, use_sample=use_sample) + return (await self.call_endpoint('gts_ship', params, use_sample=use_sample)) - def gts_buoy1( + async def gts_buoy1( self, *, use_sample: bool = False, @@ -12527,9 +12492,9 @@ def gts_buoy1( Path: /api/typ01/url/gts_buoy1.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_buoy1', params, use_sample=use_sample) + return (await self.call_endpoint('gts_buoy1', params, use_sample=use_sample)) - def gts_buoy2( + async def gts_buoy2( self, *, use_sample: bool = False, @@ -12540,9 +12505,9 @@ def gts_buoy2( Path: /api/typ01/url/gts_buoy2.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_buoy2', params, use_sample=use_sample) + return (await self.call_endpoint('gts_buoy2', params, use_sample=use_sample)) - def gts_bufr_buoy( + async def gts_bufr_buoy( self, *, use_sample: bool = False, @@ -12553,9 +12518,9 @@ def gts_bufr_buoy( Path: /api/typ01/url/gts_bufr_buoy.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_bufr_buoy', params, use_sample=use_sample) + return (await self.call_endpoint('gts_bufr_buoy', params, use_sample=use_sample)) - def gts_buoy( + async def gts_buoy( self, *, use_sample: bool = False, @@ -12566,9 +12531,9 @@ def gts_buoy( Path: /api/typ01/url/gts_buoy.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_buoy', params, use_sample=use_sample) + return (await self.call_endpoint('gts_buoy', params, use_sample=use_sample)) - def gts_temp1( + async def gts_temp1( self, *, use_sample: bool = False, @@ -12578,9 +12543,9 @@ def gts_temp1( Path: /api/typ01/url/gts_temp1.php 파라미터: tm, stn, pa, help""" - return self.call_endpoint('gts_temp1', params, use_sample=use_sample) + return (await self.call_endpoint('gts_temp1', params, use_sample=use_sample)) - def gts_bufr_temp( + async def gts_bufr_temp( self, *, use_sample: bool = False, @@ -12591,9 +12556,9 @@ def gts_bufr_temp( Path: /api/typ01/url/gts_bufr_temp.php 파라미터: tm, stn, pa, help""" - return self.call_endpoint('gts_bufr_temp', params, use_sample=use_sample) + return (await self.call_endpoint('gts_bufr_temp', params, use_sample=use_sample)) - def gts_temp( + async def gts_temp( self, *, use_sample: bool = False, @@ -12604,9 +12569,9 @@ def gts_temp( Path: /api/typ01/url/gts_temp.php 파라미터: tm, stn, pa, help""" - return self.call_endpoint('gts_temp', params, use_sample=use_sample) + return (await self.call_endpoint('gts_temp', params, use_sample=use_sample)) - def gts_pilot( + async def gts_pilot( self, *, use_sample: bool = False, @@ -12616,9 +12581,9 @@ def gts_pilot( Path: /api/typ01/url/gts_pilot.php 파라미터: tm, stn, help""" - return self.call_endpoint('gts_pilot', params, use_sample=use_sample) + return (await self.call_endpoint('gts_pilot', params, use_sample=use_sample)) - def gts_airep1( + async def gts_airep1( self, *, use_sample: bool = False, @@ -12628,9 +12593,9 @@ def gts_airep1( Path: /api/typ01/url/gts_airep1.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('gts_airep1', params, use_sample=use_sample) + return (await self.call_endpoint('gts_airep1', params, use_sample=use_sample)) - def gts_metar_dec( + async def gts_metar_dec( self, *, use_sample: bool = False, @@ -12640,9 +12605,9 @@ def gts_metar_dec( Path: /api/typ01/url/gts_metar_dec.php 파라미터: tm1, tm2, help""" - return self.call_endpoint('gts_metar_dec', params, use_sample=use_sample) + return (await self.call_endpoint('gts_metar_dec', params, use_sample=use_sample)) - def amdar_bufr( + async def amdar_bufr( self, *, use_sample: bool = False, @@ -12652,9 +12617,9 @@ def amdar_bufr( Path: /api/typ01/cgi-bin/url/nph-amdar_bufr 파라미터: flag, tm""" - return self.call_endpoint('amdar_bufr', params, use_sample=use_sample) + return (await self.call_endpoint('amdar_bufr', params, use_sample=use_sample)) - def amdar_bufr_2( + async def amdar_bufr_2( self, *, use_sample: bool = False, @@ -12665,9 +12630,9 @@ def amdar_bufr_2( Path: /api/typ01/cgi-bin/url/nph- amdar_bufr 파라미터: flag, tm, lon1, lat1, lon2, lat2, mode, pa""" - return self.call_endpoint('amdar_bufr_2', params, use_sample=use_sample) + return (await self.call_endpoint('amdar_bufr_2', params, use_sample=use_sample)) - def amdar_bufr_3( + async def amdar_bufr_3( self, *, use_sample: bool = False, @@ -12677,9 +12642,9 @@ def amdar_bufr_3( Path: /api/typ01/cgi-bin/url/nph-amdar_bufr 파라미터: flag, tm, lon1, lat1, lon2, lat2, mode""" - return self.call_endpoint('amdar_bufr_3', params, use_sample=use_sample) + return (await self.call_endpoint('amdar_bufr_3', params, use_sample=use_sample)) - def amdar_bufr_4( + async def amdar_bufr_4( self, *, use_sample: bool = False, @@ -12689,9 +12654,9 @@ def amdar_bufr_4( Path: /api/typ01/cgi-bin/url/nph-amdar_bufr 파라미터: flag, tm, aircraft, fname""" - return self.call_endpoint('amdar_bufr_4', params, use_sample=use_sample) + return (await self.call_endpoint('amdar_bufr_4', params, use_sample=use_sample)) - def gts_cht_sfc( + async def gts_cht_sfc( self, *, use_sample: bool = False, @@ -12702,9 +12667,9 @@ def gts_cht_sfc( Path: /api/typ01/url/gts_cht_sfc.php 파라미터: tm, help""" - return self.call_endpoint('gts_cht_sfc', params, use_sample=use_sample) + return (await self.call_endpoint('gts_cht_sfc', params, use_sample=use_sample)) - def gts_cht_sfc_tot( + async def gts_cht_sfc_tot( self, *, use_sample: bool = False, @@ -12715,9 +12680,9 @@ def gts_cht_sfc_tot( Path: /api/typ01/url/gts_cht_sfc_tot.php 파라미터: tm, help""" - return self.call_endpoint('gts_cht_sfc_tot', params, use_sample=use_sample) + return (await self.call_endpoint('gts_cht_sfc_tot', params, use_sample=use_sample)) - def gts_cht_syn( + async def gts_cht_syn( self, *, use_sample: bool = False, @@ -12727,9 +12692,9 @@ def gts_cht_syn( Path: /api/typ01/url/gts_cht_syn.php 파라미터: tm, help""" - return self.call_endpoint('gts_cht_syn', params, use_sample=use_sample) + return (await self.call_endpoint('gts_cht_syn', params, use_sample=use_sample)) - def gts_cht_syn_2( + async def gts_cht_syn_2( self, *, use_sample: bool = False, @@ -12740,9 +12705,9 @@ def gts_cht_syn_2( Path: /api/typ01/url/gts_cht_syn.php 파라미터: tm, lon1, lon2, lat1, lat2, help""" - return self.call_endpoint('gts_cht_syn_2', params, use_sample=use_sample) + return (await self.call_endpoint('gts_cht_syn_2', params, use_sample=use_sample)) - def gts_cht_temp( + async def gts_cht_temp( self, *, use_sample: bool = False, @@ -12752,9 +12717,9 @@ def gts_cht_temp( Path: /api/typ01/url/gts_cht_temp.php 파라미터: tm, stn, pa, lon1, lon2, lat1, lat2, help""" - return self.call_endpoint('gts_cht_temp', params, use_sample=use_sample) + return (await self.call_endpoint('gts_cht_temp', params, use_sample=use_sample)) - def gts_cht_pilot( + async def gts_cht_pilot( self, *, use_sample: bool = False, @@ -12764,9 +12729,9 @@ def gts_cht_pilot( Path: /api/typ01/url/gts_cht_pilot.php 파라미터: tm, stn, help""" - return self.call_endpoint('gts_cht_pilot', params, use_sample=use_sample) + return (await self.call_endpoint('gts_cht_pilot', params, use_sample=use_sample)) - def gts_info_service_get_buoy( + async def gts_info_service_get_buoy( self, *, use_sample: bool = False, @@ -12777,9 +12742,9 @@ def gts_info_service_get_buoy( Path: /api/typ02/openApi/GtsInfoService/getBuoy 파라미터: numOfRows, pageNo, dataType, tm, stnId""" - return self.call_endpoint('gts_info_service_get_buoy', params, use_sample=use_sample) + return (await self.call_endpoint('gts_info_service_get_buoy', params, use_sample=use_sample)) - def gts_info_service_get_synop( + async def gts_info_service_get_synop( self, *, use_sample: bool = False, @@ -12790,9 +12755,9 @@ def gts_info_service_get_synop( Path: /api/typ02/openApi/GtsInfoService/getSynop 파라미터: numOfRows, pageNo, dataType, tm, stnId""" - return self.call_endpoint('gts_info_service_get_synop', params, use_sample=use_sample) + return (await self.call_endpoint('gts_info_service_get_synop', params, use_sample=use_sample)) - def gts_info_service_get_temp( + async def gts_info_service_get_temp( self, *, use_sample: bool = False, @@ -12803,9 +12768,9 @@ def gts_info_service_get_temp( Path: /api/typ02/openApi/GtsInfoService/getTemp 파라미터: numOfRows, pageNo, dataType, tm, stnId""" - return self.call_endpoint('gts_info_service_get_temp', params, use_sample=use_sample) + return (await self.call_endpoint('gts_info_service_get_temp', params, use_sample=use_sample)) - def stn_gts1( + async def stn_gts1( self, *, use_sample: bool = False, @@ -12816,9 +12781,9 @@ def stn_gts1( Path: /api/typ01/url/stn_gts1.php 파라미터: tm, ra, stn, upp, mode""" - return self.call_endpoint('stn_gts1', params, use_sample=use_sample) + return (await self.call_endpoint('stn_gts1', params, use_sample=use_sample)) - def gts_info_service_get_gts_stn( + async def gts_info_service_get_gts_stn( self, *, use_sample: bool = False, @@ -12830,9 +12795,9 @@ def gts_info_service_get_gts_stn( /api/typ02/openApi/GtsInfoService/getGtsStn 파라미터: numOfRows, pageNo, dataType, cc, category""" - return self.call_endpoint('gts_info_service_get_gts_stn', params, use_sample=use_sample) + return (await self.call_endpoint('gts_info_service_get_gts_stn', params, use_sample=use_sample)) - def ncei_gsoh_data( + async def ncei_gsoh_data( self, *, use_sample: bool = False, @@ -12842,9 +12807,9 @@ def ncei_gsoh_data( Path: /api/typ01/url/ncei_gsoh_data.php 파라미터: tm, stns""" - return self.call_endpoint('ncei_gsoh_data', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoh_data', params, use_sample=use_sample)) - def ncei_gsoh_data_2( + async def ncei_gsoh_data_2( self, *, use_sample: bool = False, @@ -12854,9 +12819,9 @@ def ncei_gsoh_data_2( Path: /api/typ01/url/ncei_gsoh_data.php 파라미터: tm1, tm2, stns""" - return self.call_endpoint('ncei_gsoh_data_2', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoh_data_2', params, use_sample=use_sample)) - def ncei_gsoh_file( + async def ncei_gsoh_file( self, *, use_sample: bool = False, @@ -12867,9 +12832,9 @@ def ncei_gsoh_file( Path: /api/typ01/url/ncei_gsoh_file.php 파라미터: YY, stn""" - return self.call_endpoint('ncei_gsoh_file', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoh_file', params, use_sample=use_sample)) - def ncei_gsoh_list( + async def ncei_gsoh_list( self, *, use_sample: bool = False, @@ -12880,9 +12845,9 @@ def ncei_gsoh_list( Path: /api/typ01/url/ncei_gsoh_list.php 파라미터: YY""" - return self.call_endpoint('ncei_gsoh_list', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoh_list', params, use_sample=use_sample)) - def ncei_gsod_data( + async def ncei_gsod_data( self, *, use_sample: bool = False, @@ -12892,9 +12857,9 @@ def ncei_gsod_data( Path: /api/typ01/url/ncei_gsod_data.php 파라미터: tm, stns""" - return self.call_endpoint('ncei_gsod_data', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsod_data', params, use_sample=use_sample)) - def ncei_gsod_data_2( + async def ncei_gsod_data_2( self, *, use_sample: bool = False, @@ -12904,9 +12869,9 @@ def ncei_gsod_data_2( Path: /api/typ01/url/ncei_gsod_data.php 파라미터: tm1, tm2, stns""" - return self.call_endpoint('ncei_gsod_data_2', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsod_data_2', params, use_sample=use_sample)) - def ncei_gsod_data_3( + async def ncei_gsod_data_3( self, *, use_sample: bool = False, @@ -12916,9 +12881,9 @@ def ncei_gsod_data_3( Path: /api/typ01/url/ncei_gsod_data.php 파라미터: tm, lon1, lon2, lat1, lat2""" - return self.call_endpoint('ncei_gsod_data_3', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsod_data_3', params, use_sample=use_sample)) - def ncei_gsod_file( + async def ncei_gsod_file( self, *, use_sample: bool = False, @@ -12929,9 +12894,9 @@ def ncei_gsod_file( Path: /api/typ01/url/ncei_gsod_file.php 파라미터: YY, stn""" - return self.call_endpoint('ncei_gsod_file', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsod_file', params, use_sample=use_sample)) - def ncei_gsod_list( + async def ncei_gsod_list( self, *, use_sample: bool = False, @@ -12942,9 +12907,9 @@ def ncei_gsod_list( Path: /api/typ01/url/ncei_gsod_list.php 파라미터: YY""" - return self.call_endpoint('ncei_gsod_list', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsod_list', params, use_sample=use_sample)) - def ncei_gsom_data( + async def ncei_gsom_data( self, *, use_sample: bool = False, @@ -12954,9 +12919,9 @@ def ncei_gsom_data( Path: /api/typ01/url/ncei_gsom_data.php 파라미터: tm, stns""" - return self.call_endpoint('ncei_gsom_data', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsom_data', params, use_sample=use_sample)) - def ncei_gsom_data_2( + async def ncei_gsom_data_2( self, *, use_sample: bool = False, @@ -12966,9 +12931,9 @@ def ncei_gsom_data_2( Path: /api/typ01/url/ncei_gsom_data.php 파라미터: tm1, tm2, stns""" - return self.call_endpoint('ncei_gsom_data_2', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsom_data_2', params, use_sample=use_sample)) - def ncei_gsom_data_3( + async def ncei_gsom_data_3( self, *, use_sample: bool = False, @@ -12978,9 +12943,9 @@ def ncei_gsom_data_3( Path: /api/typ01/url/ncei_gsom_data.php 파라미터: tm, lon1, lon2, lat1, lat2""" - return self.call_endpoint('ncei_gsom_data_3', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsom_data_3', params, use_sample=use_sample)) - def ncei_gsom_file( + async def ncei_gsom_file( self, *, use_sample: bool = False, @@ -12991,9 +12956,9 @@ def ncei_gsom_file( Path: /api/typ01/url/ncei_gsom_file.php 파라미터: stn""" - return self.call_endpoint('ncei_gsom_file', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsom_file', params, use_sample=use_sample)) - def ncei_gsom_list( + async def ncei_gsom_list( self, *, use_sample: bool = False, @@ -13003,9 +12968,9 @@ def ncei_gsom_list( Path: /api/typ01/url/ncei_gsom_list.php 파라미터: 없음""" - return self.call_endpoint('ncei_gsom_list', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsom_list', params, use_sample=use_sample)) - def ncei_gsoy_data( + async def ncei_gsoy_data( self, *, use_sample: bool = False, @@ -13015,9 +12980,9 @@ def ncei_gsoy_data( Path: /api/typ01/url/ncei_gsoy_data.php 파라미터: tm, stns""" - return self.call_endpoint('ncei_gsoy_data', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoy_data', params, use_sample=use_sample)) - def ncei_gsoy_data_2( + async def ncei_gsoy_data_2( self, *, use_sample: bool = False, @@ -13027,9 +12992,9 @@ def ncei_gsoy_data_2( Path: /api/typ01/url/ncei_gsoy_data.php 파라미터: tm1, tm2, stns""" - return self.call_endpoint('ncei_gsoy_data_2', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoy_data_2', params, use_sample=use_sample)) - def ncei_gsoy_data_3( + async def ncei_gsoy_data_3( self, *, use_sample: bool = False, @@ -13039,9 +13004,9 @@ def ncei_gsoy_data_3( Path: /api/typ01/url/ncei_gsoy_data.php 파라미터: tm, lon1, lon2, lat1, lat2""" - return self.call_endpoint('ncei_gsoy_data_3', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoy_data_3', params, use_sample=use_sample)) - def ncei_gsoy_file( + async def ncei_gsoy_file( self, *, use_sample: bool = False, @@ -13052,9 +13017,9 @@ def ncei_gsoy_file( Path: /api/typ01/url/ncei_gsoy_file.php 파라미터: stn""" - return self.call_endpoint('ncei_gsoy_file', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoy_file', params, use_sample=use_sample)) - def ncei_gsoy_list( + async def ncei_gsoy_list( self, *, use_sample: bool = False, @@ -13064,9 +13029,9 @@ def ncei_gsoy_list( Path: /api/typ01/url/ncei_gsoy_list.php 파라미터: 없음""" - return self.call_endpoint('ncei_gsoy_list', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsoy_list', params, use_sample=use_sample)) - def ncei_upp_data( + async def ncei_upp_data( self, *, use_sample: bool = False, @@ -13076,9 +13041,9 @@ def ncei_upp_data( Path: /api/typ01/url/ncei_upp_data.php 파라미터: tm, stns""" - return self.call_endpoint('ncei_upp_data', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_upp_data', params, use_sample=use_sample)) - def ncei_upp_data_2( + async def ncei_upp_data_2( self, *, use_sample: bool = False, @@ -13088,9 +13053,9 @@ def ncei_upp_data_2( Path: /api/typ01/url/ncei_upp_data.php 파라미터: tm1, tm2, stns""" - return self.call_endpoint('ncei_upp_data_2', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_upp_data_2', params, use_sample=use_sample)) - def ncei_upp_data_3( + async def ncei_upp_data_3( self, *, use_sample: bool = False, @@ -13100,9 +13065,9 @@ def ncei_upp_data_3( Path: /api/typ01/url/ncei_upp_data.php 파라미터: tm, lon1, lon2, lat1, lat2""" - return self.call_endpoint('ncei_upp_data_3', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_upp_data_3', params, use_sample=use_sample)) - def ncei_upp_file( + async def ncei_upp_file( self, *, use_sample: bool = False, @@ -13113,9 +13078,9 @@ def ncei_upp_file( Path: /api/typ01/url/ncei_upp_file.php 파라미터: stn""" - return self.call_endpoint('ncei_upp_file', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_upp_file', params, use_sample=use_sample)) - def ncei_upp_list( + async def ncei_upp_list( self, *, use_sample: bool = False, @@ -13125,9 +13090,9 @@ def ncei_upp_list( Path: /api/typ01/url/ncei_upp_list.php 파라미터: 없음""" - return self.call_endpoint('ncei_upp_list', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_upp_list', params, use_sample=use_sample)) - def ncei_gsea_data( + async def ncei_gsea_data( self, *, use_sample: bool = False, @@ -13138,9 +13103,9 @@ def ncei_gsea_data( Path: /api/typ01/url/ncei_gsea_data.php 파라미터: tm, stns""" - return self.call_endpoint('ncei_gsea_data', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsea_data', params, use_sample=use_sample)) - def ncei_gsea_data_2( + async def ncei_gsea_data_2( self, *, use_sample: bool = False, @@ -13151,9 +13116,9 @@ def ncei_gsea_data_2( Path: /api/typ01/url/ncei_gsea_data.php 파라미터: tm1, tm2, stns""" - return self.call_endpoint('ncei_gsea_data_2', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsea_data_2', params, use_sample=use_sample)) - def ncei_gsea_data_3( + async def ncei_gsea_data_3( self, *, use_sample: bool = False, @@ -13164,9 +13129,9 @@ def ncei_gsea_data_3( Path: /api/typ01/url/ncei_gsea_data.php 파라미터: tm1, tm2, lon1, lon2, lat1, lat2""" - return self.call_endpoint('ncei_gsea_data_3', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsea_data_3', params, use_sample=use_sample)) - def ncei_gsea_file( + async def ncei_gsea_file( self, *, use_sample: bool = False, @@ -13177,9 +13142,9 @@ def ncei_gsea_file( Path: /api/typ01/url/ncei_gsea_file.php 파라미터: YM, file""" - return self.call_endpoint('ncei_gsea_file', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsea_file', params, use_sample=use_sample)) - def ncei_gsea_list( + async def ncei_gsea_list( self, *, use_sample: bool = False, @@ -13190,9 +13155,9 @@ def ncei_gsea_list( Path: /api/typ01/url/ncei_gsea_list.php 파라미터: YM""" - return self.call_endpoint('ncei_gsea_list', params, use_sample=use_sample) + return (await self.call_endpoint('ncei_gsea_list', params, use_sample=use_sample)) - def amm_iwxxm_service_get_metar( + async def amm_iwxxm_service_get_metar( self, *, use_sample: bool = False, @@ -13202,9 +13167,9 @@ def amm_iwxxm_service_get_metar( Path: /api/typ02/openApi/AmmIwxxmService/getMetar 파라미터: pageNo, numOfRows, dataType, icao""" - return self.call_endpoint('amm_iwxxm_service_get_metar', params, use_sample=use_sample) + return (await self.call_endpoint('amm_iwxxm_service_get_metar', params, use_sample=use_sample)) - def air_metar_dec( + async def air_metar_dec( self, *, use_sample: bool = False, @@ -13215,9 +13180,9 @@ def air_metar_dec( Path: /api/typ01/url/air_metar_dec.php 파라미터: tm, org, help""" - return self.call_endpoint('air_metar_dec', params, use_sample=use_sample) + return (await self.call_endpoint('air_metar_dec', params, use_sample=use_sample)) - def sfc_yearly_info_service_getr_air_stn_lst_tbl( + async def sfc_yearly_info_service_getr_air_stn_lst_tbl( self, *, use_sample: bool = False, @@ -13229,9 +13194,9 @@ def sfc_yearly_info_service_getr_air_stn_lst_tbl( /api/typ02/openApi/SfcYearlyInfoService/getrAirStnLstTbl 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_getr_air_stn_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_getr_air_stn_lst_tbl', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_air_stn_info( + async def sfc_yearly_info_service_get_air_stn_info( self, *, use_sample: bool = False, @@ -13243,9 +13208,9 @@ def sfc_yearly_info_service_get_air_stn_info( /api/typ02/openApi/SfcYearlyInfoService/getAirStnInfo 파라미터: pageNo, numOfRows, dataType, year, station""" - return self.call_endpoint('sfc_yearly_info_service_get_air_stn_info', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_air_stn_info', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_air_stn_info2( + async def sfc_yearly_info_service_get_air_stn_info2( self, *, use_sample: bool = False, @@ -13257,9 +13222,9 @@ def sfc_yearly_info_service_get_air_stn_info2( /api/typ02/openApi/SfcYearlyInfoService/getAirStnInfo2 파라미터: pageNo, numOfRows, dataType, year, station""" - return self.call_endpoint('sfc_yearly_info_service_get_air_stn_info2', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_air_stn_info2', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_air_stn_info3( + async def sfc_yearly_info_service_get_air_stn_info3( self, *, use_sample: bool = False, @@ -13271,9 +13236,9 @@ def sfc_yearly_info_service_get_air_stn_info3( /api/typ02/openApi/SfcYearlyInfoService/getAirStnInfo3 파라미터: pageNo, numOfRows, dataType, year, station""" - return self.call_endpoint('sfc_yearly_info_service_get_air_stn_info3', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_air_stn_info3', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_sfc_stn_lst_tbl( + async def sfc_yearly_info_service_get_sfc_stn_lst_tbl( self, *, use_sample: bool = False, @@ -13285,9 +13250,9 @@ def sfc_yearly_info_service_get_sfc_stn_lst_tbl( /api/typ02/openApi/SfcYearlyInfoService/getSfcStnLstTbl 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_get_sfc_stn_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_sfc_stn_lst_tbl', params, use_sample=use_sample)) - def sfc_yearly_info_service_get_note( + async def sfc_yearly_info_service_get_note( self, *, use_sample: bool = False, @@ -13297,9 +13262,9 @@ def sfc_yearly_info_service_get_note( Path: /api/typ02/openApi/SfcYearlyInfoService/getNote 파라미터: pageNo, numOfRows, dataType, year""" - return self.call_endpoint('sfc_yearly_info_service_get_note', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_yearly_info_service_get_note', params, use_sample=use_sample)) - def sfc_mtly_info_service_get_daily_air_data( + async def sfc_mtly_info_service_get_daily_air_data( self, *, use_sample: bool = False, @@ -13311,9 +13276,9 @@ def sfc_mtly_info_service_get_daily_air_data( /api/typ02/openApi/SfcMtlyInfoService/getDailyAirData 파라미터: pageNo, numOfRows, dataType, year, month, station""" - return self.call_endpoint('sfc_mtly_info_service_get_daily_air_data', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_get_daily_air_data', params, use_sample=use_sample)) - def sfc_mtly_info_service_getr_air_stn_lst_tbl( + async def sfc_mtly_info_service_getr_air_stn_lst_tbl( self, *, use_sample: bool = False, @@ -13325,9 +13290,9 @@ def sfc_mtly_info_service_getr_air_stn_lst_tbl( /api/typ02/openApi/SfcMtlyInfoService/getrAirStnLstTbl 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sfc_mtly_info_service_getr_air_stn_lst_tbl', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_getr_air_stn_lst_tbl', params, use_sample=use_sample)) - def sfc_mtly_info_service_get_air_note( + async def sfc_mtly_info_service_get_air_note( self, *, use_sample: bool = False, @@ -13339,9 +13304,9 @@ def sfc_mtly_info_service_get_air_note( /api/typ02/openApi/SfcMtlyInfoService/getAirNote 파라미터: pageNo, numOfRows, dataType, year, month""" - return self.call_endpoint('sfc_mtly_info_service_get_air_note', params, use_sample=use_sample) + return (await self.call_endpoint('sfc_mtly_info_service_get_air_note', params, use_sample=use_sample)) - def kma_air_tm( + async def kma_air_tm( self, *, use_sample: bool = False, @@ -13351,9 +13316,9 @@ def kma_air_tm( Path: /api/typ01/url/kma_air_tm.php 파라미터: tm1, tm2, stn, help""" - return self.call_endpoint('kma_air_tm', params, use_sample=use_sample) + return (await self.call_endpoint('kma_air_tm', params, use_sample=use_sample)) - def amos( + async def amos( self, *, use_sample: bool = False, @@ -13363,9 +13328,9 @@ def amos( Path: /api/typ01/url/amos.php 파라미터: tm, dtm, stn, help""" - return self.call_endpoint('amos', params, use_sample=use_sample) + return (await self.call_endpoint('amos', params, use_sample=use_sample)) - def air_info_service_get_air_info( + async def air_info_service_get_air_info( self, *, use_sample: bool = False, @@ -13375,9 +13340,9 @@ def air_info_service_get_air_info( Path: /api/typ02/openApi/AirInfoService/getAirInfo 파라미터: numOfRows, pageNo, dataType, fctm, icaoCode""" - return self.call_endpoint('air_info_service_get_air_info', params, use_sample=use_sample) + return (await self.call_endpoint('air_info_service_get_air_info', params, use_sample=use_sample)) - def amm_iwxxm_service_get_taf( + async def amm_iwxxm_service_get_taf( self, *, use_sample: bool = False, @@ -13388,9 +13353,9 @@ def amm_iwxxm_service_get_taf( Path: /api/typ02/openApi/AmmIwxxmService/getTaf 파라미터: pageNo, numOfRows, dataType, icao""" - return self.call_endpoint('amm_iwxxm_service_get_taf', params, use_sample=use_sample) + return (await self.call_endpoint('amm_iwxxm_service_get_taf', params, use_sample=use_sample)) - def amm_iwxxm_service_get_sigmet( + async def amm_iwxxm_service_get_sigmet( self, *, use_sample: bool = False, @@ -13400,9 +13365,9 @@ def amm_iwxxm_service_get_sigmet( Path: /api/typ02/openApi/AmmIwxxmService/getSigmet 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('amm_iwxxm_service_get_sigmet', params, use_sample=use_sample) + return (await self.call_endpoint('amm_iwxxm_service_get_sigmet', params, use_sample=use_sample)) - def amm_iwxxm_service_get_airmet( + async def amm_iwxxm_service_get_airmet( self, *, use_sample: bool = False, @@ -13412,9 +13377,9 @@ def amm_iwxxm_service_get_airmet( Path: /api/typ02/openApi/AmmIwxxmService/getAirmet 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('amm_iwxxm_service_get_airmet', params, use_sample=use_sample) + return (await self.call_endpoint('amm_iwxxm_service_get_airmet', params, use_sample=use_sample)) - def aftn_amm_service_get_metar( + async def aftn_amm_service_get_metar( self, *, use_sample: bool = False, @@ -13425,9 +13390,9 @@ def aftn_amm_service_get_metar( Path: /api/typ02/openApi/AftnAmmService/getMetar 파라미터: pageNo, numOfRows, dataType, icao""" - return self.call_endpoint('aftn_amm_service_get_metar', params, use_sample=use_sample) + return (await self.call_endpoint('aftn_amm_service_get_metar', params, use_sample=use_sample)) - def aftn_amm_service_get_sigmet( + async def aftn_amm_service_get_sigmet( self, *, use_sample: bool = False, @@ -13437,9 +13402,9 @@ def aftn_amm_service_get_sigmet( Path: /api/typ02/openApi/AftnAmmService/getSigmet 파라미터: pageNo, numOfRows, dataType, icao""" - return self.call_endpoint('aftn_amm_service_get_sigmet', params, use_sample=use_sample) + return (await self.call_endpoint('aftn_amm_service_get_sigmet', params, use_sample=use_sample)) - def aftn_amm_service_get_taf( + async def aftn_amm_service_get_taf( self, *, use_sample: bool = False, @@ -13450,9 +13415,9 @@ def aftn_amm_service_get_taf( Path: /api/typ02/openApi/AftnAmmService/getTaf 파라미터: pageNo, numOfRows, dataType, icao""" - return self.call_endpoint('aftn_amm_service_get_taf', params, use_sample=use_sample) + return (await self.call_endpoint('aftn_amm_service_get_taf', params, use_sample=use_sample)) - def amm_service_get_taf( + async def amm_service_get_taf( self, *, use_sample: bool = False, @@ -13463,9 +13428,9 @@ def amm_service_get_taf( Path: /api/typ02/openApi/AmmService/getTaf 파라미터: pageNo, numOfRows, dataType, icao""" - return self.call_endpoint('amm_service_get_taf', params, use_sample=use_sample) + return (await self.call_endpoint('amm_service_get_taf', params, use_sample=use_sample)) - def amm_service_get_airmet( + async def amm_service_get_airmet( self, *, use_sample: bool = False, @@ -13476,9 +13441,9 @@ def amm_service_get_airmet( Path: /api/typ02/openApi/AmmService/getAirmet 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('amm_service_get_airmet', params, use_sample=use_sample) + return (await self.call_endpoint('amm_service_get_airmet', params, use_sample=use_sample)) - def amm_service_get_sigmet( + async def amm_service_get_sigmet( self, *, use_sample: bool = False, @@ -13489,9 +13454,9 @@ def amm_service_get_sigmet( Path: /api/typ02/openApi/AmmService/getSigmet 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('amm_service_get_sigmet', params, use_sample=use_sample) + return (await self.call_endpoint('amm_service_get_sigmet', params, use_sample=use_sample)) - def amm_service_get_warning( + async def amm_service_get_warning( self, *, use_sample: bool = False, @@ -13502,9 +13467,9 @@ def amm_service_get_warning( Path: /api/typ02/openApi/AmmService/getWarning 파라미터: pageNo, numOfRows, dataType""" - return self.call_endpoint('amm_service_get_warning', params, use_sample=use_sample) + return (await self.call_endpoint('amm_service_get_warning', params, use_sample=use_sample)) - def amdar_kma( + async def amdar_kma( self, *, use_sample: bool = False, @@ -13514,9 +13479,9 @@ def amdar_kma( Path: /api/typ01/url/amdar_kma.php 파라미터: tm1, tm2, st, help""" - return self.call_endpoint('amdar_kma', params, use_sample=use_sample) + return (await self.call_endpoint('amdar_kma', params, use_sample=use_sample)) - def air_port_service_get_air_port( + async def air_port_service_get_air_port( self, *, use_sample: bool = False, @@ -13526,9 +13491,9 @@ def air_port_service_get_air_port( Path: /api/typ02/openApi/AirPortService/getAirPort 파라미터: numOfRows, pageNo, dataType, base_date, base_time, airPortCd""" - return self.call_endpoint('air_port_service_get_air_port', params, use_sample=use_sample) + return (await self.call_endpoint('air_port_service_get_air_port', params, use_sample=use_sample)) - def amo_sigwx( + async def amo_sigwx( self, *, use_sample: bool = False, @@ -13539,9 +13504,9 @@ def amo_sigwx( Path: /api/typ01/url/amo_sigwx.php 파라미터: tmfc""" - return self.call_endpoint('amo_sigwx', params, use_sample=use_sample) + return (await self.call_endpoint('amo_sigwx', params, use_sample=use_sample)) - def amo_wintem( + async def amo_wintem( self, *, use_sample: bool = False, @@ -13552,9 +13517,9 @@ def amo_wintem( Path: /api/typ01/url/amo_wintem.php 파라미터: tmfc, ef, ht""" - return self.call_endpoint('amo_wintem', params, use_sample=use_sample) + return (await self.call_endpoint('amo_wintem', params, use_sample=use_sample)) - def amo_nwp_file_down( + async def amo_nwp_file_down( self, *, use_sample: bool = False, @@ -13565,9 +13530,9 @@ def amo_nwp_file_down( Path: /api/typ01/url/amo_nwp_file_down.php 파라미터: tmfc, ef""" - return self.call_endpoint('amo_nwp_file_down', params, use_sample=use_sample) + return (await self.call_endpoint('amo_nwp_file_down', params, use_sample=use_sample)) - def lidar( + async def lidar( self, *, use_sample: bool = False, @@ -13578,7 +13543,7 @@ def lidar( Path: /api/typ01/url/lidar.php 파라미터: tm, stn, var, altitude""" - return self.call_endpoint('lidar', params, use_sample=use_sample) + return (await self.call_endpoint('lidar', params, use_sample=use_sample)) __all__ = [ diff --git a/src/kma/cli.py b/src/kma/cli.py index 304cd1d..bd59c2e 100644 --- a/src/kma/cli.py +++ b/src/kma/cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import asyncio import json import sys from collections.abc import Sequence @@ -14,6 +15,10 @@ def main(argv: Sequence[str] | None = None) -> int: + return asyncio.run(_main(argv)) + + +async def _main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="kma") parser.add_argument( "--service-key", @@ -55,11 +60,10 @@ def main(argv: Sequence[str] | None = None) -> int: if args.auth_key: _warn_argv_secret("--auth-key", "KMA_APIHUB_AUTH_KEY") apihub_client = ( - ApiHubClient(auth_key=args.auth_key) - if args.auth_key - else ApiHubClient.from_env() + ApiHubClient(auth_key=args.auth_key) if args.auth_key else ApiHubClient.from_env() ) - response = apihub_client.request_path(args.path, params) + async with apihub_client: + response = await apihub_client.request_path(args.path, params) print(response.text) return 0 @@ -67,30 +71,29 @@ def main(argv: Sequence[str] | None = None) -> int: if args.service_key: _warn_argv_secret("--service-key", "DATA_GO_KR_SERVICE_KEY") kma_client = ( - KmaClient(service_key=args.service_key) - if args.service_key - else KmaClient.from_env() + KmaClient(service_key=args.service_key) if args.service_key else KmaClient.from_env() ) - if args.command == "now": - print( - json.dumps( - _jsonable(_now(kma_client, location)), - ensure_ascii=False, - default=str, - indent=2, + async with kma_client: + if args.command == "now": + print( + json.dumps( + _jsonable(await _now(kma_client, location)), + ensure_ascii=False, + default=str, + indent=2, + ) ) + return 0 + + forecast = ( + (await _forecast_short(kma_client, location)) + if args.short + else (await _forecast(kma_client, location)) ) + print(json.dumps(_jsonable(forecast), ensure_ascii=False, default=str, indent=2)) return 0 - forecast = ( - _forecast_short(kma_client, location) - if args.short - else _forecast(kma_client, location) - ) - print(json.dumps(_jsonable(forecast), ensure_ascii=False, default=str, indent=2)) - return 0 - def _warn_argv_secret(flag: str, env_var: str) -> None: print( @@ -128,22 +131,22 @@ def _location_kwargs(args: argparse.Namespace) -> dict[str, float | int]: return {"nx": args.nx, "ny": args.ny} -def _now(client: KmaClient, location: dict[str, float | int]) -> Any: +async def _now(client: KmaClient, location: dict[str, float | int]) -> Any: if "lat" in location: - return client.now(lat=float(location["lat"]), lon=float(location["lon"])) - return client.now(nx=int(location["nx"]), ny=int(location["ny"])) + return await client.now(lat=float(location["lat"]), lon=float(location["lon"])) + return await client.now(nx=int(location["nx"]), ny=int(location["ny"])) -def _forecast_short(client: KmaClient, location: dict[str, float | int]) -> Any: +async def _forecast_short(client: KmaClient, location: dict[str, float | int]) -> Any: if "lat" in location: - return client.forecast_short(lat=float(location["lat"]), lon=float(location["lon"])) - return client.forecast_short(nx=int(location["nx"]), ny=int(location["ny"])) + return await client.forecast_short(lat=float(location["lat"]), lon=float(location["lon"])) + return await client.forecast_short(nx=int(location["nx"]), ny=int(location["ny"])) -def _forecast(client: KmaClient, location: dict[str, float | int]) -> Any: +async def _forecast(client: KmaClient, location: dict[str, float | int]) -> Any: if "lat" in location: - return client.forecast(lat=float(location["lat"]), lon=float(location["lon"])) - return client.forecast(nx=int(location["nx"]), ny=int(location["ny"])) + return await client.forecast(lat=float(location["lat"]), lon=float(location["lon"])) + return await client.forecast(nx=int(location["nx"]), ny=int(location["ny"])) def _parse_params(values: list[str]) -> dict[str, str]: diff --git a/src/kma/client.py b/src/kma/client.py index 951a074..f25ca0b 100644 --- a/src/kma/client.py +++ b/src/kma/client.py @@ -12,21 +12,22 @@ from ._credentials import DATA_GOKR_ENV_NAMES, first_env_value, normalize_api_key from ._http import ( NO_DATA_RESULT_CODE, - async_get_with_retries, build_async_client, - build_session, empty_kma_body, get_with_retries, raise_for_kma_http_error, raise_for_kma_network_error, raise_for_kma_result_code, raise_for_kma_xml_error_body, + validate_async_session, ) from ._parsing import float_or_none as _float_or_none from ._parsing import int_or_none as _int_or_none +from ._ratelimit import AsyncTokenBucket +from ._redact import credential_values, redact_exception from .codes import label_for, normalize_value, parse_amount from .enums import KmaEndpoint, WeatherCategory, coerce_category, enum_value -from .exceptions import KmaParseError +from .exceptions import KmaError, KmaParseError from .grid import validate_grid from .locations import LocationInput, normalize_location from .metadata import ResponseMetadata, make_response_metadata @@ -67,7 +68,8 @@ def __init__( retries: int = 3, base_url: str | None = None, session: Any | None = None, - async_session: Any | None = None, + max_rps: float = 5.0, + rate_limiter: AsyncTokenBucket | None = None, ) -> None: self.service_key = normalize_api_key(service_key, field_name="service_key") self.timeout = timeout @@ -75,135 +77,45 @@ def __init__( self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/") self._session = session self._owns_session = session is None - self._async_session = async_session - self._owns_async_session = async_session is None - self.forecast: ForecastService = ForecastService(self) + validate_async_session(session) + self.rate_limiter = rate_limiter if rate_limiter is not None else AsyncTokenBucket(max_rps) self.closed = False + self.forecast: ForecastService = ForecastService(self) - @classmethod - def from_env(cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any) -> KmaClient: - names = ( - DATA_GOKR_ENV_NAMES - if name == "DATA_GO_KR_SERVICE_KEY" - else (name, *DATA_GOKR_ENV_NAMES) - ) - service_key = first_env_value(names) - return cls(service_key=service_key, **kwargs) + def _ensure_open(self) -> None: + if self.closed: + raise RuntimeError("client is closed") - @classmethod - def aio(cls, service_key: str, **kwargs: Any) -> AsyncKmaClient: - """Create a client intended for async use.""" + @property + def session(self) -> Any: + return self._get_session() - return AsyncKmaClient(service_key=service_key, **kwargs) + async def aclose(self) -> None: + if not self.closed: + self.closed = True + if self._owns_session and self._session is not None: + close = getattr(self._session, "aclose", None) + if callable(close): + await close() @classmethod - def aio_from_env(cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any) -> AsyncKmaClient: - """Create an async-capable client from environment credentials.""" - + def from_env(cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any) -> KmaClient: names = ( DATA_GOKR_ENV_NAMES if name == "DATA_GO_KR_SERVICE_KEY" else (name, *DATA_GOKR_ENV_NAMES) ) service_key = first_env_value(names) - return AsyncKmaClient(service_key=service_key, **kwargs) - - @property - def session(self) -> Any: - if self._session is None: - self._session = build_session(self.retries) - return self._session - - def close(self) -> None: - if self._owns_session and self._session is not None: - close = getattr(self._session, "close", None) - if close is not None: - close() - self.closed = True - - async def aclose(self) -> None: - if self._async_session is None or not self._owns_async_session: - return - aclose = getattr(self._async_session, "aclose", None) - close = getattr(self._async_session, "close", None) - if aclose is not None: - await aclose() - elif close is not None: - close() - - def __enter__(self) -> KmaClient: - return self - - def __exit__(self, *_exc: object) -> None: - self.close() + return cls(service_key=service_key, **kwargs) async def __aenter__(self) -> KmaClient: + self._ensure_open() return self async def __aexit__(self, *_exc: object) -> None: await self.aclose() - def now( - self, - *, - location: LocationInput | None = None, - lat: float | None = None, - lon: float | None = None, - nx: int | None = None, - ny: int | None = None, - when: datetime | None = None, - ) -> WeatherSnapshot: - """초단기실황 관측값을 조회합니다. - - `when`을 생략하면 `getUltraSrtNcst`의 최신 조회 가능 KST 기준시각을 - 자동으로 선택합니다. - """ - - grid_x, grid_y = self._coordinates( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - ) - base_date, base_time = latest_ultra_srt_ncst_base(when) - fetched = self._fetch_items( - KmaEndpoint.ULTRA_SRT_NCST, - base_date=base_date, - base_time=base_time, - nx=grid_x, - ny=grid_y, - ) - items = fetched.items - by_category = {str(item.get("category")): item.get("obsrValue") for item in items} - raw = {"items": items, "by_category": by_category} - - return WeatherSnapshot( - observed_at=parse_kma_datetime(base_date, base_time), - nx=grid_x, - ny=grid_y, - temperature=_float_or_none( - by_category.get(WeatherCategory.CURRENT_TEMPERATURE.value) - ), - humidity=_int_or_none(by_category.get(WeatherCategory.HUMIDITY.value)), - wind_speed=_float_or_none(by_category.get(WeatherCategory.WIND_SPEED.value)), - wind_direction=_int_or_none(by_category.get(WeatherCategory.WIND_DIRECTION.value)), - precipitation=parse_amount(by_category.get(WeatherCategory.ONE_HOUR_RAIN.value)), - sky_label=label_for( - WeatherCategory.SKY, - by_category.get(WeatherCategory.SKY.value), - endpoint=KmaEndpoint.ULTRA_SRT_NCST, - ), - precipitation_label=label_for( - WeatherCategory.PRECIPITATION_TYPE, - by_category.get(WeatherCategory.PRECIPITATION_TYPE.value), - endpoint=KmaEndpoint.ULTRA_SRT_NCST, - ), - raw=raw, - metadata=fetched.metadata, - ) - - async def anow( + async def now( self, *, location: LocationInput | None = None, @@ -223,7 +135,7 @@ async def anow( ny=ny, ) base_date, base_time = latest_ultra_srt_ncst_base(when) - fetched = await self._afetch_items( + fetched = await self._fetch_items( KmaEndpoint.ULTRA_SRT_NCST, base_date=base_date, base_time=base_time, @@ -238,9 +150,7 @@ async def anow( observed_at=parse_kma_datetime(base_date, base_time), nx=grid_x, ny=grid_y, - temperature=_float_or_none( - by_category.get(WeatherCategory.CURRENT_TEMPERATURE.value) - ), + temperature=_float_or_none(by_category.get(WeatherCategory.CURRENT_TEMPERATURE.value)), humidity=_int_or_none(by_category.get(WeatherCategory.HUMIDITY.value)), wind_speed=_float_or_none(by_category.get(WeatherCategory.WIND_SPEED.value)), wind_direction=_int_or_none(by_category.get(WeatherCategory.WIND_DIRECTION.value)), @@ -259,39 +169,7 @@ async def anow( metadata=fetched.metadata, ) - def forecast_short( - self, - *, - location: LocationInput | None = None, - lat: float | None = None, - lon: float | None = None, - nx: int | None = None, - ny: int | None = None, - when: datetime | None = None, - ) -> list[ForecastItem]: - """`getUltraSrtFcst` 초단기예보 항목을 조회합니다.""" - - grid_x, grid_y = self._coordinates( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - ) - base_date, base_time = latest_ultra_srt_fcst_base(when) - fetched = self._fetch_items( - KmaEndpoint.ULTRA_SRT_FCST, - base_date=base_date, - base_time=base_time, - nx=grid_x, - ny=grid_y, - ) - return [ - _forecast_item(item, KmaEndpoint.ULTRA_SRT_FCST, metadata=fetched.metadata) - for item in fetched.items - ] - - async def aforecast_short( + async def forecast_short( self, *, location: LocationInput | None = None, @@ -302,60 +180,31 @@ async def aforecast_short( when: datetime | None = None, ) -> list[ForecastItem]: """Asynchronously fetch `getUltraSrtFcst` forecast items.""" - - grid_x, grid_y = self._coordinates( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - ) - base_date, base_time = latest_ultra_srt_fcst_base(when) - fetched = await self._afetch_items( - KmaEndpoint.ULTRA_SRT_FCST, - base_date=base_date, - base_time=base_time, - nx=grid_x, - ny=grid_y, - ) - return [ - _forecast_item(item, KmaEndpoint.ULTRA_SRT_FCST, metadata=fetched.metadata) - for item in fetched.items - ] - - def _forecast_vilage( - self, - *, - location: LocationInput | None = None, - lat: float | None = None, - lon: float | None = None, - nx: int | None = None, - ny: int | None = None, - when: datetime | None = None, - ) -> list[ForecastItem]: - """`getVilageFcst` 단기예보 항목을 조회합니다.""" - - grid_x, grid_y = self._coordinates( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - ) - base_date, base_time = latest_vilage_base(when) - fetched = self._fetch_items( - KmaEndpoint.VILAGE_FCST, - base_date=base_date, - base_time=base_time, - nx=grid_x, - ny=grid_y, - ) - return [ - _forecast_item(item, KmaEndpoint.VILAGE_FCST, metadata=fetched.metadata) - for item in fetched.items - ] - - async def aforecast( + try: + grid_x, grid_y = self._coordinates( + location=location, + lat=lat, + lon=lon, + nx=nx, + ny=ny, + ) + base_date, base_time = latest_ultra_srt_fcst_base(when) + fetched = await self._fetch_items( + KmaEndpoint.ULTRA_SRT_FCST, + base_date=base_date, + base_time=base_time, + nx=grid_x, + ny=grid_y, + ) + return [ + _forecast_item(item, KmaEndpoint.ULTRA_SRT_FCST, metadata=fetched.metadata) + for item in fetched.items + ] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None + + async def _forecast_vilage( self, *, location: LocationInput | None = None, @@ -366,49 +215,37 @@ async def aforecast( when: datetime | None = None, ) -> list[ForecastItem]: """Asynchronously fetch `getVilageFcst` forecast items.""" + try: + grid_x, grid_y = self._coordinates( + location=location, + lat=lat, + lon=lon, + nx=nx, + ny=ny, + ) + base_date, base_time = latest_vilage_base(when) + fetched = await self._fetch_items( + KmaEndpoint.VILAGE_FCST, + base_date=base_date, + base_time=base_time, + nx=grid_x, + ny=grid_y, + ) + return [ + _forecast_item(item, KmaEndpoint.VILAGE_FCST, metadata=fetched.metadata) + for item in fetched.items + ] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None - grid_x, grid_y = self._coordinates( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - ) - base_date, base_time = latest_vilage_base(when) - fetched = await self._afetch_items( - KmaEndpoint.VILAGE_FCST, - base_date=base_date, - base_time=base_time, - nx=grid_x, - ny=grid_y, - ) - return [ - _forecast_item(item, KmaEndpoint.VILAGE_FCST, metadata=fetched.metadata) - for item in fetched.items - ] - - def version(self, ftype: str, when: datetime) -> Mapping[str, Any]: - """`getFcstVersion` 예보 버전 metadata를 조회합니다.""" - - when_kst = as_kst(when) - base_date = when_kst.strftime("%Y%m%d") - base_time = when_kst.strftime("%H%M") - items = self._request( - KmaEndpoint.FCST_VERSION, - { - "ftype": ftype, - "basedatetime": f"{base_date}{base_time}", - }, - ) - return items - - async def aversion(self, ftype: str, when: datetime) -> Mapping[str, Any]: + async def version(self, ftype: str, when: datetime) -> Mapping[str, Any]: """Asynchronously fetch `getFcstVersion` metadata.""" when_kst = as_kst(when) base_date = when_kst.strftime("%Y%m%d") base_time = when_kst.strftime("%H%M") - return await self._arequest( + return await self._request( KmaEndpoint.FCST_VERSION, { "ftype": ftype, @@ -428,55 +265,7 @@ def _coordinates( grid = normalize_location(location, lat=lat, lon=lon, nx=nx, ny=ny) return grid.nx, grid.ny - def _fetch_items( - self, - endpoint: str | KmaEndpoint, - *, - base_date: str, - base_time: str, - nx: int, - ny: int, - ) -> _FetchedItems: - response = self._request_with_metadata( - endpoint, - { - "base_date": base_date, - "base_time": base_time, - "nx": nx, - "ny": ny, - }, - ) - if has_next_page(response.body): - raise KmaParseError( - "KMA response has more items than the requested page size", - provider="data.go.kr", - endpoint=enum_value(endpoint), - failure_kind="parse", - retryable=False, - ) - try: - items = response.body["items"]["item"] - except (KeyError, TypeError) as exc: - raise KmaParseError( - "KMA response did not contain items.item", - provider="data.go.kr", - endpoint=enum_value(endpoint), - failure_kind="parse", - retryable=False, - ) from exc - if isinstance(items, Mapping): - return _FetchedItems([items], response.metadata) - if not isinstance(items, list): - raise KmaParseError( - "KMA response items.item was not a list", - provider="data.go.kr", - endpoint=enum_value(endpoint), - failure_kind="parse", - retryable=False, - ) - return _FetchedItems(items, response.metadata) - - async def _afetch_items( + async def _fetch_items( self, endpoint: str | KmaEndpoint, *, @@ -485,7 +274,7 @@ async def _afetch_items( nx: int, ny: int, ) -> _FetchedItems: - response = await self._arequest_with_metadata( + response = await self._request_with_metadata( endpoint, { "base_date": base_date, @@ -524,214 +313,77 @@ async def _afetch_items( ) return _FetchedItems(items, response.metadata) - def _request( + async def _request( self, endpoint: str | KmaEndpoint, params: Mapping[str, Any], ) -> Mapping[str, Any]: - return self._request_with_metadata(endpoint, params).body + return (await self._request_with_metadata(endpoint, params)).body - async def _arequest( - self, - endpoint: str | KmaEndpoint, - params: Mapping[str, Any], - ) -> Mapping[str, Any]: - return (await self._arequest_with_metadata(endpoint, params)).body - - def _request_with_metadata( - self, - endpoint: str | KmaEndpoint, - params: Mapping[str, Any], - ) -> _KmaBody: - endpoint_name = enum_value(endpoint) - request_params: dict[str, Any] = { - "serviceKey": self.service_key, - "pageNo": 1, - "numOfRows": 1000, - "dataType": "JSON", - } - request_params.update(params) - metadata = make_response_metadata( - provider="data.go.kr", - service_name=SERVICE_NAME, - endpoint=endpoint_name, - request_params=request_params, - base_date=str(request_params.get("base_date")) - if request_params.get("base_date") is not None - else None, - base_time=str(request_params.get("base_time")) - if request_params.get("base_time") is not None - else None, - ) - - try: - response = get_with_retries( - self.session, - f"{self.base_url}/{endpoint_name}", - params=request_params, - timeout=self.timeout, - retries=self.retries, - ) - except httpx.HTTPStatusError as exc: - raise_for_kma_http_error( - exc, - provider="data.go.kr", - endpoint=endpoint_name, - label="KMA", - ) - except httpx.RequestError: - raise_for_kma_network_error( - provider="data.go.kr", - endpoint=endpoint_name, - label="KMA", - ) - - return _parse_kma_body(response, endpoint_name, metadata) - - async def _arequest_with_metadata( + async def _request_with_metadata( self, endpoint: str | KmaEndpoint, params: Mapping[str, Any], ) -> _KmaBody: - endpoint_name = enum_value(endpoint) - request_params: dict[str, Any] = { - "serviceKey": self.service_key, - "pageNo": 1, - "numOfRows": 1000, - "dataType": "JSON", - } - request_params.update(params) - metadata = make_response_metadata( - provider="data.go.kr", - service_name=SERVICE_NAME, - endpoint=endpoint_name, - request_params=request_params, - base_date=str(request_params.get("base_date")) - if request_params.get("base_date") is not None - else None, - base_time=str(request_params.get("base_time")) - if request_params.get("base_time") is not None - else None, - ) - try: - response = await async_get_with_retries( - self._get_async_session(), - f"{self.base_url}/{endpoint_name}", - params=request_params, - timeout=self.timeout, - retries=self.retries, - ) - except httpx.HTTPStatusError as exc: - raise_for_kma_http_error( - exc, - provider="data.go.kr", - endpoint=endpoint_name, - label="KMA", - ) - except httpx.RequestError: - raise_for_kma_network_error( + endpoint_name = enum_value(endpoint) + request_params: dict[str, Any] = { + "serviceKey": self.service_key, + "pageNo": 1, + "numOfRows": 1000, + "dataType": "JSON", + } + request_params.update(params) + metadata = make_response_metadata( provider="data.go.kr", + service_name=SERVICE_NAME, endpoint=endpoint_name, - label="KMA", + request_params=request_params, + base_date=str(request_params.get("base_date")) + if request_params.get("base_date") is not None + else None, + base_time=str(request_params.get("base_time")) + if request_params.get("base_time") is not None + else None, ) - return _parse_kma_body(response, endpoint_name, metadata) - - def _get_async_session(self) -> Any: - if self._async_session is None: - self._async_session = build_async_client() - return self._async_session + try: + response = await get_with_retries( + self._get_session(), + f"{self.base_url}/{endpoint_name}", + params=request_params, + timeout=self.timeout, + retries=self.retries, + rate_limiter=self.rate_limiter, + ensure_open=self._ensure_open, + ) + except httpx.HTTPStatusError as exc: + raise_for_kma_http_error( + exc, + provider="data.go.kr", + endpoint=endpoint_name, + label="KMA", + ) + except httpx.RequestError: + raise_for_kma_network_error( + provider="data.go.kr", + endpoint=endpoint_name, + label="KMA", + ) + + return _parse_kma_body(response, endpoint_name, metadata) + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(params)) + raise exc from None + + def _get_session(self) -> Any: + self._ensure_open() + if self._session is None: + self._session = build_async_client() + return self._session class ForecastService: - """Service facade for KMA short-term forecast endpoints.""" - - def __init__(self, client: KmaClient) -> None: - self._client = client - - def __call__( - self, - *, - location: LocationInput | None = None, - lat: float | None = None, - lon: float | None = None, - nx: int | None = None, - ny: int | None = None, - when: datetime | None = None, - ) -> list[ForecastItem]: - return self.vilage( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - when=when, - ) - - def now( - self, - *, - location: LocationInput | None = None, - lat: float | None = None, - lon: float | None = None, - nx: int | None = None, - ny: int | None = None, - when: datetime | None = None, - ) -> WeatherSnapshot: - return KmaClient.now( - self._client, - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - when=when, - ) - - def short( - self, - *, - location: LocationInput | None = None, - lat: float | None = None, - lon: float | None = None, - nx: int | None = None, - ny: int | None = None, - when: datetime | None = None, - ) -> list[ForecastItem]: - return self._client.forecast_short( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - when=when, - ) - - def vilage( - self, - *, - location: LocationInput | None = None, - lat: float | None = None, - lon: float | None = None, - nx: int | None = None, - ny: int | None = None, - when: datetime | None = None, - ) -> list[ForecastItem]: - return self._client._forecast_vilage( - location=location, - lat=lat, - lon=lon, - nx=nx, - ny=ny, - when=when, - ) - - def version(self, ftype: str, when: datetime) -> Mapping[str, Any]: - return self._client.version(ftype, when) - - -class AsyncForecastService: """Async service facade for KMA short-term forecast endpoints.""" def __init__(self, client: KmaClient) -> None: @@ -766,7 +418,7 @@ async def now( ny: int | None = None, when: datetime | None = None, ) -> WeatherSnapshot: - return await self._client.anow( + return await self._client.now( location=location, lat=lat, lon=lon, @@ -785,7 +437,7 @@ async def short( ny: int | None = None, when: datetime | None = None, ) -> list[ForecastItem]: - return await self._client.aforecast_short( + return await self._client.forecast_short( location=location, lat=lat, lon=lon, @@ -804,7 +456,7 @@ async def vilage( ny: int | None = None, when: datetime | None = None, ) -> list[ForecastItem]: - return await self._client.aforecast( + return await self._client._forecast_vilage( location=location, lat=lat, lon=lon, @@ -814,57 +466,7 @@ async def vilage( ) async def version(self, ftype: str, when: datetime) -> Mapping[str, Any]: - return await self._client.aversion(ftype, when) - - -class AsyncKmaClient: - """Asynchronous facade for KMA public weather APIs.""" - - def __init__( - self, - service_key: str, - *, - timeout: float = 10, - retries: int = 3, - base_url: str | None = None, - async_session: Any | None = None, - ) -> None: - self._client = KmaClient( - service_key, - timeout=timeout, - retries=retries, - base_url=base_url, - async_session=async_session, - ) - self.service_key = self._client.service_key - self.config = { - "base_url": self._client.base_url, - "timeout": self._client.timeout, - "retries": self._client.retries, - } - self.forecast = AsyncForecastService(self._client) - self.closed = False - - @classmethod - def from_env(cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any) -> AsyncKmaClient: - names = ( - DATA_GOKR_ENV_NAMES - if name == "DATA_GO_KR_SERVICE_KEY" - else (name, *DATA_GOKR_ENV_NAMES) - ) - service_key = first_env_value(names) - return cls(service_key=service_key, **kwargs) - - async def __aenter__(self) -> AsyncKmaClient: - return self - - async def __aexit__(self, *_exc: object) -> None: - await self.aclose() - - async def aclose(self) -> None: - await self._client.aclose() - self._client.close() - self.closed = True + return await self._client.version(ftype, when) def _parse_kma_body(response: Any, endpoint_name: str, metadata: ResponseMetadata) -> _KmaBody: diff --git a/src/kma/datagokr.py b/src/kma/datagokr.py index 9cddf6c..d2e2013 100644 --- a/src/kma/datagokr.py +++ b/src/kma/datagokr.py @@ -3,7 +3,7 @@ from __future__ import annotations import time -from collections.abc import AsyncIterator, Callable, Iterator, Mapping +from collections.abc import AsyncIterator, Callable, Mapping from dataclasses import dataclass from datetime import date, datetime from typing import Any @@ -13,19 +13,21 @@ from ._credentials import DATA_GOKR_ENV_NAMES, first_env_value, normalize_api_key from ._http import ( NO_DATA_RESULT_CODE, - async_get_with_retries, build_async_client, - build_session, empty_kma_body, get_with_retries, raise_for_kma_http_error, raise_for_kma_network_error, raise_for_kma_result_code, raise_for_kma_xml_error_body, + register_credential_param, + validate_async_session, ) from ._parsing import float_or_none as _float_or_none from ._parsing import int_or_none as _int_or_none from ._parsing import str_or_none as _str_or_none +from ._ratelimit import AsyncTokenBucket +from ._redact import credential_values, redact_debug, redact_exception from .catalog import ApiCatalogEntry, api_catalog from .codes import label_for, normalize_value from .datagokr_catalog import ( @@ -35,7 +37,7 @@ ) from .debug import DebugRun, debug_error, jsonable, redact_sensitive from .enums import coerce_category -from .exceptions import KmaParseError +from .exceptions import KmaError, KmaParseError from .metadata import ResponseMetadata, make_response_metadata from .models import ( AsosDailyItem, @@ -49,7 +51,6 @@ MidForecastItem, WeatherWarningItem, ) -from .pagination import aiter_pages as _aiter_pages from .pagination import iter_pages as _iter_pages from .time_utils import ( KST, @@ -102,7 +103,8 @@ def __init__( base_url: str = DATA_GOKR_BASE_URL, service_key_param: str = "serviceKey", session: Any | None = None, - async_session: Any | None = None, + max_rps: float = 5.0, + rate_limiter: AsyncTokenBucket | None = None, ) -> None: if not service_key_param: raise ValueError("service_key_param is required") @@ -111,10 +113,28 @@ def __init__( self.timeout = timeout self.retries = retries self.base_url = base_url.rstrip("/") - self.session = session or build_session(retries) + register_credential_param(self.service_key_param) + self._session = session self._owns_session = session is None - self._async_session = async_session - self._owns_async_session = async_session is None + validate_async_session(session) + self.rate_limiter = rate_limiter if rate_limiter is not None else AsyncTokenBucket(max_rps) + self.closed = False + + def _ensure_open(self) -> None: + if self.closed: + raise RuntimeError("client is closed") + + @property + def session(self) -> Any: + return self._get_session() + + async def aclose(self) -> None: + if not self.closed: + self.closed = True + if self._owns_session and self._session is not None: + close = getattr(self._session, "aclose", None) + if callable(close): + await close() @classmethod def from_env(cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any) -> DataGoKrClient: @@ -126,73 +146,14 @@ def from_env(cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any) -> DataGo service_key = first_env_value(names) return cls(service_key, **kwargs) - @classmethod - def aio(cls, service_key: str, **kwargs: Any) -> AsyncDataGoKrClient: - """Create an async facade for the data.go.kr gateway.""" - - return AsyncDataGoKrClient(service_key, **kwargs) - - @classmethod - def aio_from_env( - cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any - ) -> AsyncDataGoKrClient: - """Create an async facade from environment credentials.""" - - return AsyncDataGoKrClient.from_env(name=name, **kwargs) - - def close(self) -> None: - close = getattr(self.session, "close", None) - if self._owns_session and close is not None: - close() - - async def aclose(self) -> None: - if self._async_session is None or not self._owns_async_session: - return - aclose = getattr(self._async_session, "aclose", None) - close = getattr(self._async_session, "close", None) - if aclose is not None: - await aclose() - elif close is not None: - close() - - def __enter__(self) -> DataGoKrClient: - return self - - def __exit__(self, *_exc: object) -> None: - self.close() - async def __aenter__(self) -> DataGoKrClient: + self._ensure_open() return self async def __aexit__(self, *_exc: object) -> None: await self.aclose() - def request( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> Mapping[str, Any]: - """JSON 응답을 지원하는 data.go.kr 기상청 service operation을 호출합니다. - - `service`는 `MidFcstInfoService` 같은 path segment이고, - `operation`은 `getMidFcst` 같은 endpoint입니다. - """ - - return self._request_with_metadata( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ).body - - async def arequest( + async def request( self, service: str, operation: str, @@ -205,7 +166,7 @@ async def arequest( """Asynchronously call a data.go.kr KMA service operation.""" return ( - await self._arequest_with_metadata( + await self._request_with_metadata( service, operation, params, @@ -215,29 +176,7 @@ async def arequest( ) ).body - def request_with_metadata( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> tuple[Mapping[str, Any], ResponseMetadata]: - """service operation을 호출하고 `(body, metadata)`를 반환합니다.""" - - response = self._request_with_metadata( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return response.body, response.metadata - - async def arequest_with_metadata( + async def request_with_metadata( self, service: str, operation: str, @@ -249,7 +188,7 @@ async def arequest_with_metadata( ) -> tuple[Mapping[str, Any], ResponseMetadata]: """Asynchronously call a service operation and return `(body, metadata)`.""" - response = await self._arequest_with_metadata( + response = await self._request_with_metadata( service, operation, params, @@ -259,7 +198,7 @@ async def arequest_with_metadata( ) return response.body, response.metadata - def debug_fetch( + async def debug_fetch( self, service: str, operation: str, @@ -310,7 +249,7 @@ def debug_fetch( started_at = time.monotonic() try: - body, metadata = self.request_with_metadata( + body, metadata = await self.request_with_metadata( clean_service, clean_operation, clean_params, @@ -333,15 +272,19 @@ def debug_fetch( except Exception as exc: elapsed_ms = (time.monotonic() - started_at) * 1000 trace.append(f"요청 실패: {exc.__class__.__name__} ({elapsed_ms:.0f}ms)") - return DebugRun( - function=endpoint, - input=input_data, - request=request_info, - response={}, - parsed=None, - processed=None, - trace=trace, - error=debug_error(exc), + return redact_debug( + DebugRun( + function=endpoint, + input=input_data, + request=request_info, + response={}, + parsed=None, + processed=None, + trace=trace, + error=debug_error(exc), + ), + self.service_key, + *credential_values(params, self.service_key_param), ) elapsed_ms = (time.monotonic() - started_at) * 1000 @@ -354,17 +297,21 @@ def debug_fetch( parsed_model = ( models if isinstance(processed, list) else (models[0] if models else jsonable(body)) ) - return DebugRun( - function=endpoint, - input=input_data, - request={**request_info, "query": redact_sensitive(dict(metadata.request_params))}, - response={"status_code": 200, "body": jsonable(body)}, - parsed=parsed_model, - processed=processed, - trace=trace, + return redact_debug( + DebugRun( + function=endpoint, + input=input_data, + request={**request_info, "query": redact_sensitive(dict(metadata.request_params))}, + response={"status_code": 200, "body": jsonable(body)}, + parsed=parsed_model, + processed=processed, + trace=trace, + ), + self.service_key, + *credential_values(params, self.service_key_param), ) - def _request_with_metadata( + async def _request_with_metadata( self, service: str, operation: str, @@ -374,169 +321,87 @@ def _request_with_metadata( page_no: int = 1, num_of_rows: int = 10, ) -> _DataGoKrBody: - clean_service = service.strip("/") - clean_operation = operation.strip("/") - endpoint = f"{clean_service}/{clean_operation}" - request_params: dict[str, Any] = { - self.service_key_param: self.service_key, - "pageNo": page_no, - "numOfRows": num_of_rows, - "dataType": data_type, - } - if params: - request_params.update(params) - metadata_params = { - key: value for key, value in request_params.items() if key != self.service_key_param - } - metadata = make_response_metadata( - provider="data.go.kr", - service_name=clean_service, - endpoint=endpoint, - request_params=metadata_params, - base_date=_metadata_param(request_params, "base_date", "Base_date"), - base_time=str(request_params.get("base_time")) - if request_params.get("base_time") is not None - else None, - ) - try: - response = get_with_retries( - self.session, - f"{self.base_url}/{endpoint}", - params=request_params, - timeout=self.timeout, - retries=self.retries, - ) - except httpx.HTTPStatusError as exc: - raise_for_kma_http_error( - exc, - provider="data.go.kr", - endpoint=endpoint, - label="data.go.kr", - ) - except httpx.RequestError: - raise_for_kma_network_error( + clean_service = service.strip("/") + clean_operation = operation.strip("/") + endpoint = f"{clean_service}/{clean_operation}" + request_params: dict[str, Any] = { + self.service_key_param: self.service_key, + "pageNo": page_no, + "numOfRows": num_of_rows, + "dataType": data_type, + } + if params: + request_params.update(params) + metadata_params = { + key: value for key, value in request_params.items() if key != self.service_key_param + } + metadata = make_response_metadata( provider="data.go.kr", + service_name=clean_service, endpoint=endpoint, - label="data.go.kr", + request_params=metadata_params, + base_date=_metadata_param(request_params, "base_date", "Base_date"), + base_time=str(request_params.get("base_time")) + if request_params.get("base_time") is not None + else None, ) - try: - payload = response.json() - except ValueError as exc: - if raise_for_kma_xml_error_body( - response.text, - provider="data.go.kr", - endpoint=endpoint, - label="data.go.kr", - ): - return _DataGoKrBody(empty_kma_body(), metadata) - raise KmaParseError( - "data.go.kr response was not JSON", - provider="data.go.kr", - endpoint=endpoint, - status_code=response.status_code, - failure_kind="parse", - retryable=False, - ) from exc - return _DataGoKrBody( - _unwrap_data_gokr_payload(payload, endpoint=endpoint, status_code=response.status_code), - metadata, - ) - - async def _arequest_with_metadata( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> _DataGoKrBody: - clean_service = service.strip("/") - clean_operation = operation.strip("/") - endpoint = f"{clean_service}/{clean_operation}" - request_params: dict[str, Any] = { - self.service_key_param: self.service_key, - "pageNo": page_no, - "numOfRows": num_of_rows, - "dataType": data_type, - } - if params: - request_params.update(params) - metadata_params = { - key: value for key, value in request_params.items() if key != self.service_key_param - } - metadata = make_response_metadata( - provider="data.go.kr", - service_name=clean_service, - endpoint=endpoint, - request_params=metadata_params, - base_date=_metadata_param(request_params, "base_date", "Base_date"), - base_time=str(request_params.get("base_time")) - if request_params.get("base_time") is not None - else None, - ) + try: + response = await get_with_retries( + self._get_session(), + f"{self.base_url}/{endpoint}", + params=request_params, + timeout=self.timeout, + retries=self.retries, + rate_limiter=self.rate_limiter, + ensure_open=self._ensure_open, + ) + except httpx.HTTPStatusError as exc: + raise_for_kma_http_error( + exc, + provider="data.go.kr", + endpoint=endpoint, + label="data.go.kr", + ) + except httpx.RequestError: + raise_for_kma_network_error( + provider="data.go.kr", + endpoint=endpoint, + label="data.go.kr", + ) - try: - response = await async_get_with_retries( - self._get_async_session(), - f"{self.base_url}/{endpoint}", - params=request_params, - timeout=self.timeout, - retries=self.retries, + try: + payload = response.json() + except ValueError as exc: + if raise_for_kma_xml_error_body( + response.text, + provider="data.go.kr", + endpoint=endpoint, + label="data.go.kr", + ): + return _DataGoKrBody(empty_kma_body(), metadata) + raise KmaParseError( + "data.go.kr response was not JSON", + provider="data.go.kr", + endpoint=endpoint, + status_code=response.status_code, + failure_kind="parse", + retryable=False, + ) from exc + return _DataGoKrBody( + _unwrap_data_gokr_payload( + payload, endpoint=endpoint, status_code=response.status_code + ), + metadata, ) - except httpx.HTTPStatusError as exc: - raise_for_kma_http_error( - exc, - provider="data.go.kr", - endpoint=endpoint, - label="data.go.kr", + except KmaError as exc: + redact_exception( + exc, self.service_key, *credential_values(params, self.service_key_param) ) - except httpx.RequestError: - raise_for_kma_network_error( - provider="data.go.kr", - endpoint=endpoint, - label="data.go.kr", - ) - - try: - payload = response.json() - except ValueError as exc: - if raise_for_kma_xml_error_body( - response.text, - provider="data.go.kr", - endpoint=endpoint, - label="data.go.kr", - ): - return _DataGoKrBody(empty_kma_body(), metadata) - raise KmaParseError( - "data.go.kr response was not JSON", - provider="data.go.kr", - endpoint=endpoint, - status_code=response.status_code, - failure_kind="parse", - retryable=False, - ) from exc - return _DataGoKrBody( - _unwrap_data_gokr_payload(payload, endpoint=endpoint, status_code=response.status_code), - metadata, - ) + raise exc from None - def items( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - **kwargs: Any, - ) -> list[Mapping[str, Any]]: - """operation을 호출하고 `response.body.items.item`을 list로 반환합니다.""" - - body = self.request(service, operation, params, **kwargs) - return _items_from_body(body, endpoint=f"{service.strip('/')}/{operation.strip('/')}") - - async def aitems( + async def items( self, service: str, operation: str, @@ -544,9 +409,14 @@ async def aitems( **kwargs: Any, ) -> list[Mapping[str, Any]]: """Asynchronously return `response.body.items.item` as a list.""" - - body = await self.arequest(service, operation, params, **kwargs) - return _items_from_body(body, endpoint=f"{service.strip('/')}/{operation.strip('/')}") + try: + body = await self.request(service, operation, params, **kwargs) + return _items_from_body(body, endpoint=f"{service.strip('/')}/{operation.strip('/')}") + except KmaError as exc: + redact_exception( + exc, self.service_key, *credential_values(params, self.service_key_param) + ) + raise exc from None def datasets(self) -> tuple[DataGoKrDatasetSpec, ...]: """공공데이터포털의 기상청 data.go.kr OpenAPI dataset 목록을 반환합니다.""" @@ -572,7 +442,7 @@ def dataset(self, dataset_id: str | int) -> DataGoKrDatasetSpec: except KeyError: raise ValueError(f"unknown KMA data.go.kr dataset_id: {clean_id}") from None - def request_dataset( + async def request_dataset( self, dataset_id: str | int, params: Mapping[str, Any] | None = None, @@ -585,29 +455,7 @@ def request_dataset( """공공데이터포털 dataset id로 기상청 data.go.kr dataset을 호출합니다.""" _, service, selected_operation = self._dataset_service_operation(dataset_id, operation) - return self.request( - service, - selected_operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - - def dataset_items( - self, - dataset_id: str | int, - params: Mapping[str, Any] | None = None, - *, - operation: str | None = None, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> list[DataGoKrItem]: - """기상청 data.go.kr dataset을 호출하고 metadata가 있는 원본 row를 반환합니다.""" - - _, service, selected_operation = self._dataset_service_operation(dataset_id, operation) - return self._raw_items( + return await self.request( service, selected_operation, params, @@ -616,7 +464,7 @@ def dataset_items( num_of_rows=num_of_rows, ) - async def adataset_items( + async def dataset_items( self, dataset_id: str | int, params: Mapping[str, Any] | None = None, @@ -629,7 +477,7 @@ async def adataset_items( """Asynchronously call a data.go.kr dataset and return metadata-bearing rows.""" _, service, selected_operation = self._dataset_service_operation(dataset_id, operation) - return await self._araw_items( + return await self._raw_items( service, selected_operation, params, @@ -638,35 +486,7 @@ async def adataset_items( num_of_rows=num_of_rows, ) - def iter_pages( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - start_page: int = 1, - num_of_rows: int = 10, - max_pages: int = 100, - max_items: int | None = None, - ) -> Iterator[Mapping[str, Any]]: - """명시적 안전장치와 함께 data.go.kr 페이지네이션 응답 body를 순회합니다.""" - - return _iter_pages( - lambda page_no: self.request( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ), - start_page=start_page, - max_pages=max_pages, - max_items=max_items, - ) - - async def aiter_pages( + async def iter_pages( self, service: str, operation: str, @@ -680,8 +500,8 @@ async def aiter_pages( ) -> AsyncIterator[Mapping[str, Any]]: """Asynchronously iterate paginated data.go.kr response bodies.""" - async for body in _aiter_pages( - lambda page_no: self.arequest( + async for body in _iter_pages( + lambda page_no: self.request( service, operation, params, @@ -695,7 +515,7 @@ async def aiter_pages( ): yield body - def mid_forecast( + async def mid_forecast( self, *, stn_id: str | int, @@ -706,14 +526,14 @@ def mid_forecast( ) -> list[MidForecastItem]: """지역을 추정하지 않고 `MidFcstInfoService/getMidFcst`를 호출합니다.""" - return self._mid_items( + return await self._mid_items( "getMidFcst", {"stnId": str(stn_id), "tmFc": _resolve_tm_fc(tm_fc, when=when)}, page_no=page_no, num_of_rows=num_of_rows, ) - def mid_land_forecast( + async def mid_land_forecast( self, *, reg_id: str, @@ -728,14 +548,14 @@ def mid_land_forecast( `kma`는 두 식별자 사이의 mapping을 추정하거나 유지하지 않습니다. """ - return self._mid_items( + return await self._mid_items( "getMidLandFcst", {"regId": reg_id, "tmFc": _resolve_tm_fc(tm_fc, when=when)}, page_no=page_no, num_of_rows=num_of_rows, ) - def mid_temperature_forecast( + async def mid_temperature_forecast( self, *, reg_id: str, @@ -749,14 +569,14 @@ def mid_temperature_forecast( `reg_id`는 단기예보 `nx`/`ny` 격자 좌표가 아닙니다. """ - return self._mid_items( + return await self._mid_items( "getMidTa", {"regId": reg_id, "tmFc": _resolve_tm_fc(tm_fc, when=when)}, page_no=page_no, num_of_rows=num_of_rows, ) - def mid_sea_forecast( + async def mid_sea_forecast( self, *, reg_id: str, @@ -767,14 +587,14 @@ def mid_sea_forecast( ) -> list[MidForecastItem]: """기상청 중기해상예보 `reg_id`로 `getMidSeaFcst`를 호출합니다.""" - return self._mid_items( + return await self._mid_items( "getMidSeaFcst", {"regId": reg_id, "tmFc": _resolve_tm_fc(tm_fc, when=when)}, page_no=page_no, num_of_rows=num_of_rows, ) - def asos_daily_weather( + async def asos_daily_weather( self, *, start_dt: str | date | datetime, @@ -795,7 +615,7 @@ def asos_daily_weather( } if stn_ids is not None: params["stnIds"] = str(stn_ids) - fetched = self._items_with_metadata( + fetched = await self._items_with_metadata( ASOS_DAILY_SERVICE, "getWthrDataList", params, @@ -804,7 +624,7 @@ def asos_daily_weather( ) return [_asos_daily_item(row, fetched.metadata) for row in fetched.items] - def asos_hourly_weather( + async def asos_hourly_weather( self, *, start_dt: str | date | datetime, @@ -829,7 +649,7 @@ def asos_hourly_weather( } if stn_ids is not None: params["stnIds"] = str(stn_ids) - fetched = self._items_with_metadata( + fetched = await self._items_with_metadata( ASOS_HOURLY_SERVICE, "getWthrDataList", params, @@ -838,7 +658,7 @@ def asos_hourly_weather( ) return [_asos_hourly_item(row, fetched.metadata) for row in fetched.items] - def weather_warning( + async def weather_warning( self, operation: str, params: Mapping[str, Any] | None = None, @@ -854,7 +674,7 @@ def weather_warning( `getPwnStatus`가 있습니다. """ - return self._raw_items( + return await self._raw_items( WTHR_WRN_SERVICE, operation, params, @@ -862,7 +682,7 @@ def weather_warning( num_of_rows=num_of_rows, ) - def weather_warning_list( + async def weather_warning_list( self, *, stn_id: str | int, @@ -873,7 +693,7 @@ def weather_warning_list( ) -> list[WeatherWarningItem]: """`WthrWrnInfoService/getWthrWrnList`를 호출하고 타입 row를 반환합니다.""" - fetched = self._items_with_metadata( + fetched = await self._items_with_metadata( WTHR_WRN_SERVICE, "getWthrWrnList", _date_range_params(stn_id=stn_id, from_tm_fc=from_tm_fc, to_tm_fc=to_tm_fc), @@ -882,7 +702,7 @@ def weather_warning_list( ) return [_weather_warning_item(row, fetched.metadata) for row in fetched.items] - def forecast_message( + async def forecast_message( self, operation: str, params: Mapping[str, Any] | None = None, @@ -892,7 +712,7 @@ def forecast_message( ) -> list[DataGoKrItem]: """`VilageFcstMsgService` operation을 호출합니다.""" - return self._raw_items( + return await self._raw_items( VILAGE_FCST_MSG_SERVICE, operation, params, @@ -900,7 +720,7 @@ def forecast_message( num_of_rows=num_of_rows, ) - def weather_situation( + async def weather_situation( self, *, stn_id: str | int, @@ -909,14 +729,14 @@ def weather_situation( ) -> list[DataGoKrItem]: """`VilageFcstMsgService/getWthrSituation`을 호출합니다.""" - return self.forecast_message( + return await self.forecast_message( "getWthrSituation", {"stnId": str(stn_id)}, page_no=page_no, num_of_rows=num_of_rows, ) - def land_forecast_message( + async def land_forecast_message( self, *, reg_id: str, @@ -925,14 +745,14 @@ def land_forecast_message( ) -> list[DataGoKrItem]: """`VilageFcstMsgService/getLandFcst`를 호출합니다.""" - return self.forecast_message( + return await self.forecast_message( "getLandFcst", {"regId": reg_id}, page_no=page_no, num_of_rows=num_of_rows, ) - def sea_forecast_message( + async def sea_forecast_message( self, *, reg_id: str, @@ -941,14 +761,14 @@ def sea_forecast_message( ) -> list[DataGoKrItem]: """`VilageFcstMsgService/getSeaFcst`를 호출합니다.""" - return self.forecast_message( + return await self.forecast_message( "getSeaFcst", {"regId": reg_id}, page_no=page_no, num_of_rows=num_of_rows, ) - def beach_ultra_short_forecast( + async def beach_ultra_short_forecast( self, *, beach_num: str | int, @@ -963,30 +783,33 @@ def beach_ultra_short_forecast( `base_date`와 `base_time`을 생략하면 `KmaClient.forecast_short()`와 같은 규칙으로 최신 조회 가능 KST 초단기예보 base time을 선택합니다. """ + try: + base_date_text, base_time_text = _resolve_base_date_time( + base_date, + base_time, + latest_ultra_srt_fcst_base, + when=when, + ) + fetched = await self._items_with_metadata( + BEACH_INFO_SERVICE, + BEACH_ULTRA_SRT_FCST, + { + "base_date": base_date_text, + "base_time": base_time_text, + "beach_num": _format_beach_num(beach_num), + }, + page_no=page_no, + num_of_rows=num_of_rows, + ) + return [ + _beach_forecast_item(row, BEACH_ULTRA_SRT_FCST, metadata=fetched.metadata) + for row in fetched.items + ] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None - base_date_text, base_time_text = _resolve_base_date_time( - base_date, - base_time, - latest_ultra_srt_fcst_base, - when=when, - ) - fetched = self._items_with_metadata( - BEACH_INFO_SERVICE, - BEACH_ULTRA_SRT_FCST, - { - "base_date": base_date_text, - "base_time": base_time_text, - "beach_num": _format_beach_num(beach_num), - }, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return [ - _beach_forecast_item(row, BEACH_ULTRA_SRT_FCST, metadata=fetched.metadata) - for row in fetched.items - ] - - def beach_forecast( + async def beach_forecast( self, *, beach_num: str | int, @@ -1001,30 +824,33 @@ def beach_forecast( `base_date`와 `base_time`을 생략하면 `KmaClient.forecast()`와 같은 규칙으로 최신 조회 가능 KST 단기예보 base time을 선택합니다. """ + try: + base_date_text, base_time_text = _resolve_base_date_time( + base_date, + base_time, + latest_vilage_base, + when=when, + ) + fetched = await self._items_with_metadata( + BEACH_INFO_SERVICE, + BEACH_VILAGE_FCST, + { + "base_date": base_date_text, + "base_time": base_time_text, + "beach_num": _format_beach_num(beach_num), + }, + page_no=page_no, + num_of_rows=num_of_rows, + ) + return [ + _beach_forecast_item(row, BEACH_VILAGE_FCST, metadata=fetched.metadata) + for row in fetched.items + ] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None - base_date_text, base_time_text = _resolve_base_date_time( - base_date, - base_time, - latest_vilage_base, - when=when, - ) - fetched = self._items_with_metadata( - BEACH_INFO_SERVICE, - BEACH_VILAGE_FCST, - { - "base_date": base_date_text, - "base_time": base_time_text, - "beach_num": _format_beach_num(beach_num), - }, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return [ - _beach_forecast_item(row, BEACH_VILAGE_FCST, metadata=fetched.metadata) - for row in fetched.items - ] - - def beach_wave_height( + async def beach_wave_height( self, *, beach_num: str | int, @@ -1033,20 +859,23 @@ def beach_wave_height( num_of_rows: int = 10, ) -> list[BeachWaveHeight]: """해수욕장 파고 조회용 `BeachInfoservice/getWhBuoyBeach`를 호출합니다.""" + try: + fetched = await self._items_with_metadata( + BEACH_INFO_SERVICE, + BEACH_WAVE_HEIGHT, + { + "beach_num": _format_beach_num(beach_num), + "searchTime": _format_yyyymmddhhmm(search_time), + }, + page_no=page_no, + num_of_rows=num_of_rows, + ) + return [_beach_wave_height(row, metadata=fetched.metadata) for row in fetched.items] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None - fetched = self._items_with_metadata( - BEACH_INFO_SERVICE, - BEACH_WAVE_HEIGHT, - { - "beach_num": _format_beach_num(beach_num), - "searchTime": _format_yyyymmddhhmm(search_time), - }, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return [_beach_wave_height(row, metadata=fetched.metadata) for row in fetched.items] - - def beach_tide_info( + async def beach_tide_info( self, *, beach_num: str | int, @@ -1055,20 +884,23 @@ def beach_tide_info( num_of_rows: int = 10, ) -> list[BeachTideItem]: """해수욕장 조석 조회용 `BeachInfoservice/getTideInfoBeach`를 호출합니다.""" + try: + fetched = await self._items_with_metadata( + BEACH_INFO_SERVICE, + BEACH_TIDE_INFO, + { + "base_date": _format_yyyymmdd(base_date), + "beach_num": _format_beach_num(beach_num), + }, + page_no=page_no, + num_of_rows=num_of_rows, + ) + return [_beach_tide_item(row, metadata=fetched.metadata) for row in fetched.items] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None - fetched = self._items_with_metadata( - BEACH_INFO_SERVICE, - BEACH_TIDE_INFO, - { - "base_date": _format_yyyymmdd(base_date), - "beach_num": _format_beach_num(beach_num), - }, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return [_beach_tide_item(row, metadata=fetched.metadata) for row in fetched.items] - - def beach_sun_info( + async def beach_sun_info( self, *, beach_num: str | int, @@ -1081,20 +913,23 @@ def beach_sun_info( upstream Swagger는 다른 해수욕장 endpoint와 달리 요청 날짜 이름을 `Base_date`로 표기합니다. """ + try: + fetched = await self._items_with_metadata( + BEACH_INFO_SERVICE, + BEACH_SUN_INFO, + { + "Base_date": _format_yyyymmdd(base_date), + "beach_num": _format_beach_num(beach_num), + }, + page_no=page_no, + num_of_rows=num_of_rows, + ) + return [_beach_sun_time(row, metadata=fetched.metadata) for row in fetched.items] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None - fetched = self._items_with_metadata( - BEACH_INFO_SERVICE, - BEACH_SUN_INFO, - { - "Base_date": _format_yyyymmdd(base_date), - "beach_num": _format_beach_num(beach_num), - }, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return [_beach_sun_time(row, metadata=fetched.metadata) for row in fetched.items] - - def beach_water_temperature( + async def beach_water_temperature( self, *, beach_num: str | int, @@ -1103,20 +938,25 @@ def beach_water_temperature( num_of_rows: int = 10, ) -> list[BeachWaterTemperature]: """해수욕장 수온 조회용 `BeachInfoservice/getTwBuoyBeach`를 호출합니다.""" + try: + fetched = await self._items_with_metadata( + BEACH_INFO_SERVICE, + BEACH_WATER_TEMPERATURE, + { + "beach_num": _format_beach_num(beach_num), + "searchTime": _format_yyyymmddhhmm(search_time), + }, + page_no=page_no, + num_of_rows=num_of_rows, + ) + return [ + _beach_water_temperature(row, metadata=fetched.metadata) for row in fetched.items + ] + except KmaError as exc: + redact_exception(exc, self.service_key, *credential_values(None)) + raise exc from None - fetched = self._items_with_metadata( - BEACH_INFO_SERVICE, - BEACH_WATER_TEMPERATURE, - { - "beach_num": _format_beach_num(beach_num), - "searchTime": _format_yyyymmddhhmm(search_time), - }, - page_no=page_no, - num_of_rows=num_of_rows, - ) - return [_beach_water_temperature(row, metadata=fetched.metadata) for row in fetched.items] - - def tour_village_forecast( + async def tour_village_forecast( self, *, course_id: str | int, @@ -1127,7 +967,7 @@ def tour_village_forecast( ) -> list[DataGoKrItem]: """`TourStnInfoService1/getTourStnVilageFcst1`을 호출합니다.""" - return self._raw_items( + return await self._raw_items( TOUR_STN_SERVICE, "getTourStnVilageFcst1", { @@ -1139,7 +979,7 @@ def tour_village_forecast( num_of_rows=num_of_rows, ) - def city_tour_climate_index( + async def city_tour_climate_index( self, *, city_area_id: str | int, @@ -1150,7 +990,7 @@ def city_tour_climate_index( ) -> list[DataGoKrItem]: """`TourStnInfoService1/getCityTourClmIdx1`을 호출합니다.""" - return self._raw_items( + return await self._raw_items( TOUR_STN_SERVICE, "getCityTourClmIdx1", { @@ -1162,7 +1002,7 @@ def city_tour_climate_index( num_of_rows=num_of_rows, ) - def sensible_temperature_index( + async def sensible_temperature_index( self, *, area_no: str | int, @@ -1173,7 +1013,7 @@ def sensible_temperature_index( ) -> list[DataGoKrItem]: """`LivingWthrIdxServiceV4/getSenTaIdxV4`를 호출합니다.""" - return self._living_weather_index( + return await self._living_weather_index( "getSenTaIdxV4", area_no=area_no, time=time, @@ -1182,7 +1022,7 @@ def sensible_temperature_index( num_of_rows=num_of_rows, ) - def uv_index( + async def uv_index( self, *, area_no: str | int, @@ -1192,7 +1032,7 @@ def uv_index( ) -> list[DataGoKrItem]: """`LivingWthrIdxServiceV4/getUVIdxV4`를 호출합니다.""" - return self._living_weather_index( + return await self._living_weather_index( "getUVIdxV4", area_no=area_no, time=time, @@ -1200,7 +1040,7 @@ def uv_index( num_of_rows=num_of_rows, ) - def air_diffusion_index( + async def air_diffusion_index( self, *, area_no: str | int, @@ -1210,7 +1050,7 @@ def air_diffusion_index( ) -> list[DataGoKrItem]: """`LivingWthrIdxServiceV4/getAirDiffusionIdxV4`를 호출합니다.""" - return self._living_weather_index( + return await self._living_weather_index( "getAirDiffusionIdxV4", area_no=area_no, time=time, @@ -1218,7 +1058,7 @@ def air_diffusion_index( num_of_rows=num_of_rows, ) - def earthquake_info( + async def earthquake_info( self, operation: str, *, @@ -1229,7 +1069,7 @@ def earthquake_info( ) -> list[DataGoKrItem]: """`EqkInfoService` operation을 호출합니다.""" - return self._raw_items( + return await self._raw_items( EQK_INFO_SERVICE, operation, {"fromTmFc": _format_yyyymmdd(from_tm_fc), "toTmFc": _format_yyyymmdd(to_tm_fc)}, @@ -1237,7 +1077,7 @@ def earthquake_info( num_of_rows=num_of_rows, ) - def earthquake_message( + async def earthquake_message( self, *, from_tm_fc: str | date | datetime, @@ -1247,7 +1087,7 @@ def earthquake_message( ) -> list[DataGoKrItem]: """`EqkInfoService/getEqkMsg`를 호출합니다.""" - return self.earthquake_info( + return await self.earthquake_info( "getEqkMsg", from_tm_fc=from_tm_fc, to_tm_fc=to_tm_fc, @@ -1255,7 +1095,7 @@ def earthquake_message( num_of_rows=num_of_rows, ) - def earthquake_message_list( + async def earthquake_message_list( self, *, from_tm_fc: str | date | datetime, @@ -1265,7 +1105,7 @@ def earthquake_message_list( ) -> list[DataGoKrItem]: """`EqkInfoService/getEqkMsgList`를 호출합니다.""" - return self.earthquake_info( + return await self.earthquake_info( "getEqkMsgList", from_tm_fc=from_tm_fc, to_tm_fc=to_tm_fc, @@ -1273,7 +1113,7 @@ def earthquake_message_list( num_of_rows=num_of_rows, ) - def tsunami_message( + async def tsunami_message( self, *, from_tm_fc: str | date | datetime, @@ -1283,7 +1123,7 @@ def tsunami_message( ) -> list[DataGoKrItem]: """`EqkInfoService/getTsunamiMsg`를 호출합니다.""" - return self.earthquake_info( + return await self.earthquake_info( "getTsunamiMsg", from_tm_fc=from_tm_fc, to_tm_fc=to_tm_fc, @@ -1291,7 +1131,7 @@ def tsunami_message( num_of_rows=num_of_rows, ) - def tsunami_message_list( + async def tsunami_message_list( self, *, from_tm_fc: str | date | datetime, @@ -1301,7 +1141,7 @@ def tsunami_message_list( ) -> list[DataGoKrItem]: """`EqkInfoService/getTsunamiMsgList`를 호출합니다.""" - return self.earthquake_info( + return await self.earthquake_info( "getTsunamiMsgList", from_tm_fc=from_tm_fc, to_tm_fc=to_tm_fc, @@ -1309,7 +1149,7 @@ def tsunami_message_list( num_of_rows=num_of_rows, ) - def _living_weather_index( + async def _living_weather_index( self, operation: str, *, @@ -1325,7 +1165,7 @@ def _living_weather_index( } if request_code is not None: params["requestCode"] = request_code - return self._raw_items( + return await self._raw_items( LIVING_WTHR_IDX_SERVICE, operation, params, @@ -1341,8 +1181,7 @@ def _dataset_service_operation( spec = self.dataset(dataset_id) if spec.gateway != "datagokr" or spec.service is None: raise ValueError( - f"{spec.dataset_id} is APIHub-linked; use ApiHubClient " - "or ApiHubGeneratedClient" + f"{spec.dataset_id} is APIHub-linked; use ApiHubClient or ApiHubGeneratedClient" ) if operation is None: if len(spec.operations) == 1: @@ -1360,7 +1199,7 @@ def _dataset_service_operation( raise ValueError("operation is required") return spec, spec.service, selected_operation - def _raw_items( + async def _raw_items( self, service: str, operation: str, @@ -1370,7 +1209,7 @@ def _raw_items( page_no: int = 1, num_of_rows: int = 10, ) -> list[DataGoKrItem]: - fetched = self._items_with_metadata( + fetched = await self._items_with_metadata( service, operation, params, @@ -1390,37 +1229,7 @@ def _raw_items( for row in fetched.items ] - async def _araw_items( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> list[DataGoKrItem]: - fetched = await self._aitems_with_metadata( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - clean_service = service.strip("/") - clean_operation = operation.strip("/") - return [ - DataGoKrItem( - service=clean_service, - operation=clean_operation, - raw=dict(row), - metadata=fetched.metadata, - ) - for row in fetched.items - ] - - def _items_with_metadata( + async def _items_with_metadata( self, service: str, operation: str, @@ -1430,45 +1239,27 @@ def _items_with_metadata( page_no: int = 1, num_of_rows: int = 10, ) -> _DataGoKrItems: - response = self._request_with_metadata( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - endpoint = f"{service.strip('/')}/{operation.strip('/')}" - return _DataGoKrItems( - _items_from_body(response.body, endpoint=endpoint), - response.metadata, - ) - - async def _aitems_with_metadata( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> _DataGoKrItems: - response = await self._arequest_with_metadata( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - endpoint = f"{service.strip('/')}/{operation.strip('/')}" - return _DataGoKrItems( - _items_from_body(response.body, endpoint=endpoint), - response.metadata, - ) + try: + response = await self._request_with_metadata( + service, + operation, + params, + data_type=data_type, + page_no=page_no, + num_of_rows=num_of_rows, + ) + endpoint = f"{service.strip('/')}/{operation.strip('/')}" + return _DataGoKrItems( + _items_from_body(response.body, endpoint=endpoint), + response.metadata, + ) + except KmaError as exc: + redact_exception( + exc, self.service_key, *credential_values(params, self.service_key_param) + ) + raise exc from None - def _mid_items( + async def _mid_items( self, operation: str, params: Mapping[str, Any], @@ -1476,7 +1267,7 @@ def _mid_items( page_no: int, num_of_rows: int, ) -> list[MidForecastItem]: - fetched = self._items_with_metadata( + fetched = await self._items_with_metadata( MID_FCST_SERVICE, operation, params, @@ -1491,139 +1282,11 @@ def _mid_items( for row in fetched.items ] - def _get_async_session(self) -> Any: - if self._async_session is None: - self._async_session = build_async_client() - return self._async_session - - -class AsyncDataGoKrClient: - """Asynchronous facade for the data.go.kr KMA gateway. - - Mirrors :class:`AsyncKmaClient`: ``DataGoKrClient.aio()`` returns one of - these, exposing the same method names as the synchronous client but as - coroutines (delegating to the ``a``-prefixed methods underneath). - """ - - def __init__(self, service_key: str, **kwargs: Any) -> None: - self._client = DataGoKrClient(service_key, **kwargs) - self.service_key = self._client.service_key - self.config = { - "base_url": self._client.base_url, - "timeout": self._client.timeout, - "retries": self._client.retries, - } - self.closed = False - - @classmethod - def from_env(cls, name: str = "DATA_GO_KR_SERVICE_KEY", **kwargs: Any) -> AsyncDataGoKrClient: - names = ( - DATA_GOKR_ENV_NAMES - if name == "DATA_GO_KR_SERVICE_KEY" - else (name, *DATA_GOKR_ENV_NAMES) - ) - service_key = first_env_value(names) - return cls(service_key, **kwargs) - - async def __aenter__(self) -> AsyncDataGoKrClient: - return self - - async def __aexit__(self, *_exc: object) -> None: - await self.aclose() - - async def aclose(self) -> None: - await self._client.aclose() - self._client.close() - self.closed = True - - async def request( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> Mapping[str, Any]: - return await self._client.arequest( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - - async def request_with_metadata( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> tuple[Mapping[str, Any], ResponseMetadata]: - return await self._client.arequest_with_metadata( - service, - operation, - params, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - - async def items( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - **kwargs: Any, - ) -> list[Mapping[str, Any]]: - return await self._client.aitems(service, operation, params, **kwargs) - - async def dataset_items( - self, - dataset_id: str | int, - params: Mapping[str, Any] | None = None, - *, - operation: str | None = None, - data_type: str = "JSON", - page_no: int = 1, - num_of_rows: int = 10, - ) -> list[DataGoKrItem]: - return await self._client.adataset_items( - dataset_id, - params, - operation=operation, - data_type=data_type, - page_no=page_no, - num_of_rows=num_of_rows, - ) - - def iter_pages( - self, - service: str, - operation: str, - params: Mapping[str, Any] | None = None, - *, - data_type: str = "JSON", - start_page: int = 1, - num_of_rows: int = 10, - max_pages: int = 100, - max_items: int | None = None, - ) -> AsyncIterator[Mapping[str, Any]]: - return self._client.aiter_pages( - service, - operation, - params, - data_type=data_type, - start_page=start_page, - num_of_rows=num_of_rows, - max_pages=max_pages, - max_items=max_items, - ) + def _get_session(self) -> Any: + self._ensure_open() + if self._session is None: + self._session = build_async_client() + return self._session def _resolve_base_date_time( @@ -1869,8 +1532,6 @@ def _required_text(value: object, field: str) -> str: return text - - def _items_from_body(body: Mapping[str, Any], *, endpoint: str) -> list[Mapping[str, Any]]: try: raw_items = body["items"]["item"] @@ -2003,8 +1664,6 @@ def _resolve_tm_fc(value: str | datetime | None, *, when: datetime | None) -> st return _format_tm_fc(value) - - def _unwrap_data_gokr_payload( payload: Mapping[str, Any], *, diff --git a/src/kma/pagination.py b/src/kma/pagination.py index 844a18d..2436883 100644 --- a/src/kma/pagination.py +++ b/src/kma/pagination.py @@ -3,7 +3,7 @@ from __future__ import annotations import warnings -from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping +from collections.abc import AsyncIterator, Awaitable, Callable, Mapping from typing import Any @@ -30,52 +30,7 @@ def next_page_no(body: Mapping[str, Any]) -> int | None: return _int_from_body(body, "pageNo", default=1) + 1 -def iter_pages( - fetch_page: Callable[[int], Mapping[str, Any]], - *, - start_page: int = 1, - max_pages: int = 100, - max_items: int | None = None, -) -> Iterator[Mapping[str, Any]]: - """`pageNo` metadata를 따라 data.go.kr 응답 body를 순회합니다. - - `max_pages`와 `max_items`는 upstream API가 일관되지 않은 페이지네이션 - metadata를 반환할 때 무한 루프를 막는 명시적 안전장치입니다. - """ - - if start_page < 1: - raise ValueError("start_page must be >= 1") - if max_pages < 1: - raise ValueError("max_pages must be >= 1") - if max_items is not None and max_items < 1: - raise ValueError("max_items must be >= 1") - - page_no = start_page - pages_seen = 0 - items_seen = 0 - while True: - body = fetch_page(page_no) - yield body - - pages_seen += 1 - items_seen += _item_count(body) - if max_items is not None and items_seen >= max_items: - return - - if not has_next_page(body): - return - if pages_seen >= max_pages: - warnings.warn( - f"iter_pages stopped after max_pages={max_pages} pages while " - "more pages were still available; results may be incomplete", - PaginationLimitWarning, - stacklevel=2, - ) - return - page_no += 1 - - -async def aiter_pages( +async def iter_pages( fetch_page: Callable[[int], Awaitable[Mapping[str, Any]]], *, start_page: int = 1, @@ -111,7 +66,7 @@ async def aiter_pages( return if pages_seen >= max_pages: warnings.warn( - f"aiter_pages stopped after max_pages={max_pages} pages while " + f"iter_pages stopped after max_pages={max_pages} pages while " "more pages were still available; results may be incomplete", PaginationLimitWarning, stacklevel=2, diff --git a/tests/test_apihub.py b/tests/test_apihub.py index ad2831e..d4c0870 100644 --- a/tests/test_apihub.py +++ b/tests/test_apihub.py @@ -1,6 +1,6 @@ from __future__ import annotations -import asyncio +import inspect import json import warnings from typing import Any, Callable @@ -9,7 +9,6 @@ from kma.apihub import ( ApiHubClient, - AsyncApiHubClient, detect_image_info, extract_apihub_endpoints, parse_apihub_sample_url, @@ -65,7 +64,7 @@ def __init__(self, text: str) -> None: self.text = text self.calls: list[dict[str, Any]] = [] - def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: self.calls.append({"url": url, "params": params, "timeout": timeout}) return FakeResponse(self.text, url=url) @@ -87,7 +86,7 @@ async def get( class FakeErrorSession(FakeSession): - def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: self.calls.append({"url": url, "params": params, "timeout": timeout}) return FakeErrorResponse("error") @@ -118,7 +117,7 @@ def __init__(self, *, total_count: int, num_of_rows: int = 10) -> None: self.num_of_rows = num_of_rows self.calls: list[dict[str, Any]] = [] - def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: self.calls.append({"url": url, "params": params, "timeout": timeout}) assert params is not None page_no = int(params["pageNo"]) @@ -138,12 +137,14 @@ async def get( params: dict[str, Any] | None, timeout: float, ) -> FakeResponse: - return super().get(url, params=params, timeout=timeout) + return await super().get(url, params=params, timeout=timeout) -def assert_raises(exc_type: type[BaseException], func: Callable[[], object]) -> BaseException: +async def assert_raises(exc_type: type[BaseException], func: Callable[[], object]) -> BaseException: try: - func() + result = func() + if inspect.isawaitable(result): + await result except exc_type as exc: return exc raise AssertionError(f"expected {exc_type.__name__}") @@ -216,11 +217,11 @@ def test_extract_apihub_endpoints_deduplicates_generated_urls() -> None: assert endpoints[1].parameters == ("reg", "wrn") -def test_apihub_request_path_appends_auth_key() -> None: +async def test_apihub_request_path_appends_auth_key() -> None: session = FakeSession("ok") client = ApiHubClient("hub-key", session=session) - response = client.request_path("/api/typ01/url/kma_sfctm2.php", {"tm": "202211300900"}) + response = await client.request_path("/api/typ01/url/kma_sfctm2.php", {"tm": "202211300900"}) assert response.text == "ok" assert response.metadata is not None @@ -231,12 +232,12 @@ def test_apihub_request_path_appends_auth_key() -> None: assert session.calls[0]["params"] == {"authKey": "hub-key", "tm": "202211300900"} -def test_apihub_async_request_path_appends_auth_key() -> None: +async def test_apihub_async_request_path_appends_auth_key() -> None: async def run() -> None: session = AsyncFakeSession("ok") - client = ApiHubClient("hub-key", async_session=session) + client = ApiHubClient("hub-key", session=session) - response = await client.arequest_path( + response = await client.request_path( "/api/typ01/url/kma_sfctm2.php", {"tm": "202211300900"}, ) @@ -245,15 +246,15 @@ async def run() -> None: assert session.calls[0]["url"] == "https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php" assert session.calls[0]["params"] == {"authKey": "hub-key", "tm": "202211300900"} - asyncio.run(run()) + await run() -def test_apihub_aio_returns_async_facade() -> None: +async def test_apihub_aio_returns_async_facade() -> None: async def run() -> None: session = AsyncFakeSession("ok") - client = ApiHubClient.aio("hub-key", async_session=session) + client = ApiHubClient("hub-key", session=session) - assert isinstance(client, AsyncApiHubClient) + assert isinstance(client, ApiHubClient) assert client.auth_key == "hub-key" async with client: @@ -266,23 +267,23 @@ async def run() -> None: assert session.calls[0]["params"] == {"authKey": "hub-key", "tm": "202211300900"} assert client.closed is True - asyncio.run(run()) + await run() -def test_apihub_auth_key_strips_copied_whitespace() -> None: +async def test_apihub_auth_key_strips_copied_whitespace() -> None: session = FakeSession("ok") client = ApiHubClient(" hub \n key\t", session=session) - client.request_path("/api/typ01/url/kma_sfctm2.php") + (await client.request_path("/api/typ01/url/kma_sfctm2.php")) assert session.calls[0]["params"] == {"authKey": "hubkey"} -def test_apihub_open_api_builds_typ02_path_and_defaults() -> None: +async def test_apihub_open_api_builds_typ02_path_and_defaults() -> None: session = FakeSession('{"response": "ok"}') client = ApiHubClient("hub-key", session=session) - response = client.open_api( + response = await client.open_api( "MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}, @@ -296,14 +297,16 @@ def test_apihub_open_api_builds_typ02_path_and_defaults() -> None: assert session.calls[0]["params"]["numOfRows"] == 10 -def test_apihub_request_query_parts_preserves_legacy_bare_query() -> None: +async def test_apihub_request_query_parts_preserves_legacy_bare_query() -> None: session = FakeSession("image") client = ApiHubClient("hub-key", session=session) - client.request_query_parts( - "/api/typ03/cgi/aws3/nph-awsm_tms_h06", - (("bare", "tm"), ("bare", "mode"), ("named", "_DT")), - {"tm": "202305031000", "mode": "0", "_DT": "RSW:AWSCHART"}, + ( + await client.request_query_parts( + "/api/typ03/cgi/aws3/nph-awsm_tms_h06", + (("bare", "tm"), ("bare", "mode"), ("named", "_DT")), + {"tm": "202305031000", "mode": "0", "_DT": "RSW:AWSCHART"}, + ) ) assert session.calls[0]["url"] == ( @@ -313,16 +316,16 @@ def test_apihub_request_query_parts_preserves_legacy_bare_query() -> None: assert session.calls[0]["params"] is None -def test_apihub_rejects_non_api_paths() -> None: +async def test_apihub_rejects_non_api_paths() -> None: client = ApiHubClient("hub-key", session=FakeSession("ok")) - assert_raises(ValueError, lambda: client.request_path("/noticeList.do")) + (await assert_raises(ValueError, lambda: client.request_path("/noticeList.do"))) -def test_apihub_403_maps_to_auth_error_without_chained_url() -> None: +async def test_apihub_403_maps_to_auth_error_without_chained_url() -> None: client = ApiHubClient("hub-key", session=FakeErrorSession("error")) - error = assert_raises( + error = await assert_raises( KmaAuthError, lambda: client.request_path("/api/typ01/url/kma_sfctm2.php", {"tm": "202211300900"}), ) @@ -334,11 +337,7 @@ def test_apihub_403_maps_to_auth_error_without_chained_url() -> None: def test_parse_apihub_text_table_uses_comment_header() -> None: - table = parse_apihub_text_table( - "# TM STN TA\n" - "202605010000 108 17.5\n" - "202605010100 108 18.0\n" - ) + table = parse_apihub_text_table("# TM STN TA\n202605010000 108 17.5\n202605010100 108 18.0\n") assert table.headers == ("TM", "STN", "TA") assert table.rows[0]["TM"] == "202605010000" @@ -357,10 +356,7 @@ def test_detect_image_info_reads_png_dimensions() -> None: content = ( b"\x89PNG\r\n\x1a\n" b"\x00\x00\x00\r" - b"IHDR" - + (640).to_bytes(4, "big") - + (480).to_bytes(4, "big") - + b"\x08\x02\x00\x00\x00" + b"IHDR" + (640).to_bytes(4, "big") + (480).to_bytes(4, "big") + b"\x08\x02\x00\x00\x00" ) assert detect_image_info(content) == ("png", 640, 480) @@ -385,15 +381,15 @@ def test_redact_url_credentials_handles_generic_key_name() -> None: ) -def test_apihub_discover_services_and_endpoints_use_portal_pages() -> None: +async def test_apihub_discover_services_and_endpoints_use_portal_pages() -> None: service_html = 'const apiList = [{"seqApi":288,"nmApi":"기상특보"}];' endpoint_html = "https://apihub.kma.go.kr/api/typ01/url/wrn_reg.php?tmfc=0&authKey=secret" session = FakeSession(service_html) client = ApiHubClient("hub-key", session=session) - services = client.discover_services((10,)) + services = await client.discover_services((10,)) session.text = endpoint_html - endpoints = client.discover_endpoints(services[0].category_id, services[0].service_id) + endpoints = await client.discover_endpoints(services[0].category_id, services[0].service_id) assert services[0].service_name == "기상특보" assert endpoints[0].path == "/api/typ01/url/wrn_reg.php" @@ -402,40 +398,39 @@ def test_apihub_discover_services_and_endpoints_use_portal_pages() -> None: assert session.calls[1]["params"] == {"seqApi": 10, "seqApiSub": 288} -def test_apihub_iter_pages_collects_all_pages_without_warning() -> None: +async def test_apihub_iter_pages_collects_all_pages_without_warning() -> None: session = PagingFakeSession(total_count=25, num_of_rows=10) client = ApiHubClient("hub-key", session=session) with warnings.catch_warnings(): warnings.simplefilter("error", PaginationLimitWarning) - pages = list( - client.iter_pages("MidFcstInfoService", "getMidFcst", num_of_rows=10) - ) + pages = [ + item + async for item in client.iter_pages("MidFcstInfoService", "getMidFcst", num_of_rows=10) + ] assert [page["pageNo"] for page in pages] == [1, 2, 3] assert len(pages[-1]["items"]["item"]) == 5 -def test_apihub_aiter_pages_collects_all_pages_without_warning() -> None: +async def test_apihub_aiter_pages_collects_all_pages_without_warning() -> None: async def run() -> list[dict[str, Any]]: session = AsyncPagingFakeSession(total_count=25, num_of_rows=10) - client = ApiHubClient("hub-key", async_session=session) + client = ApiHubClient("hub-key", session=session) pages = [] - async for page in client.aiter_pages( - "MidFcstInfoService", "getMidFcst", num_of_rows=10 - ): + async for page in client.iter_pages("MidFcstInfoService", "getMidFcst", num_of_rows=10): pages.append(page) return pages with warnings.catch_warnings(): warnings.simplefilter("error", PaginationLimitWarning) - pages = asyncio.run(run()) + pages = await run() assert [page["pageNo"] for page in pages] == [1, 2, 3] assert len(pages[-1]["items"]["item"]) == 5 -def test_apihub_aiter_pages_warns_on_truncation_like_sync_iter_pages() -> None: +async def test_apihub_aiter_pages_warns_on_truncation_like_sync_iter_pages() -> None: """비동기 aiter_pages는 동기 iter_pages와 동일하게 max_pages 절단을 경고해야 한다. 회귀 방지 대상: 이전에는 aiter_pages가 pagination.aiter_pages를 거치지 않고 @@ -446,26 +441,27 @@ def test_apihub_aiter_pages_warns_on_truncation_like_sync_iter_pages() -> None: sync_client = ApiHubClient("hub-key", session=sync_session) with warnings.catch_warnings(record=True) as sync_caught: warnings.simplefilter("always") - sync_pages = list( - sync_client.iter_pages( + sync_pages = [ + item + async for item in sync_client.iter_pages( "MidFcstInfoService", "getMidFcst", num_of_rows=10, max_pages=2 ) - ) + ] async def run_async() -> tuple[list[dict[str, Any]], list[warnings.WarningMessage]]: async_session = AsyncPagingFakeSession(total_count=50, num_of_rows=10) - async_client = ApiHubClient("hub-key", async_session=async_session) + async_client = ApiHubClient("hub-key", session=async_session) with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") pages = [ page - async for page in async_client.aiter_pages( + async for page in async_client.iter_pages( "MidFcstInfoService", "getMidFcst", num_of_rows=10, max_pages=2 ) ] return pages, caught - async_pages, async_caught = asyncio.run(run_async()) + async_pages, async_caught = await run_async() assert len(sync_pages) == 2 assert len(async_pages) == 2 @@ -473,28 +469,28 @@ async def run_async() -> tuple[list[dict[str, Any]], list[warnings.WarningMessag assert any(issubclass(w.category, PaginationLimitWarning) for w in async_caught) -def test_apihub_aiter_pages_validates_arguments_like_sync_iter_pages() -> None: +async def test_apihub_aiter_pages_validates_arguments_like_sync_iter_pages() -> None: session = PagingFakeSession(total_count=10, num_of_rows=10) sync_client = ApiHubClient("hub-key", session=session) - sync_error = assert_raises( + sync_error = await assert_raises( ValueError, - lambda: list( - sync_client.iter_pages("MidFcstInfoService", "getMidFcst", max_pages=0) - ), + lambda: _collect(sync_client.iter_pages("MidFcstInfoService", "getMidFcst", max_pages=0)), ) assert "max_pages" in str(sync_error) async def run() -> BaseException: async_session = AsyncPagingFakeSession(total_count=10, num_of_rows=10) - async_client = ApiHubClient("hub-key", async_session=async_session) + async_client = ApiHubClient("hub-key", session=async_session) try: - async for _ in async_client.aiter_pages( - "MidFcstInfoService", "getMidFcst", max_pages=0 - ): + async for _ in async_client.iter_pages("MidFcstInfoService", "getMidFcst", max_pages=0): pass except ValueError as exc: return exc raise AssertionError("expected ValueError") - async_error = asyncio.run(run()) + async_error = await run() assert "max_pages" in str(async_error) + + +async def _collect(iterator): + return [item async for item in iterator] diff --git a/tests/test_apihub_endpoints.py b/tests/test_apihub_endpoints.py index 56689cd..2c5ba68 100644 --- a/tests/test_apihub_endpoints.py +++ b/tests/test_apihub_endpoints.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio from typing import Any from kma.apihub_endpoints import ( @@ -34,7 +33,7 @@ def __init__(self, content: bytes = b"ok") -> None: self.content = content self.calls: list[dict[str, Any]] = [] - def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: self.calls.append({"url": url, "params": params, "timeout": timeout}) return FakeResponse(self.content, url=url) @@ -44,9 +43,7 @@ def __init__(self, content: bytes = b"ok") -> None: self.content = content self.calls: list[dict[str, Any]] = [] - async def get( - self, url: str, *, params: dict[str, Any] | None, timeout: float - ) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any] | None, timeout: float) -> FakeResponse: self.calls.append({"url": url, "params": params, "timeout": timeout}) return FakeResponse(self.content, url=url) @@ -69,11 +66,11 @@ def test_generated_attachment_metadata_includes_format_and_sample_links() -> Non ) -def test_generated_named_wrapper_calls_standard_endpoint() -> None: +async def test_generated_named_wrapper_calls_standard_endpoint() -> None: session = FakeSession() client = ApiHubGeneratedClient("hub-key", session=session) - response = client.kma_sfctm2(tm="202605010900", stn="108", help="1") + response = await client.kma_sfctm2(tm="202605010900", stn="108", help="1") assert response.text == "ok" assert session.calls[0]["url"] == "https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php" @@ -85,11 +82,11 @@ def test_generated_named_wrapper_calls_standard_endpoint() -> None: } -def test_generated_named_wrapper_can_use_sample_params() -> None: +async def test_generated_named_wrapper_can_use_sample_params() -> None: session = FakeSession() client = ApiHubGeneratedClient("hub-key", session=session) - client.kma_sfctm2(use_sample=True, stn="108") + (await client.kma_sfctm2(use_sample=True, stn="108")) assert session.calls[0]["params"] == { "authKey": "hub-key", @@ -99,49 +96,44 @@ def test_generated_named_wrapper_can_use_sample_params() -> None: } -def test_generated_wrapper_preserves_bare_query_order() -> None: +async def test_generated_wrapper_preserves_bare_query_order() -> None: session = FakeSession() client = ApiHubGeneratedClient("hub-key", session=session) - client.aws3_nph_awsm_tms_h06(use_sample=True, arg2="1") + (await client.aws3_nph_awsm_tms_h06(use_sample=True, arg2="1")) call = session.calls[0] assert call["params"] is None assert call["url"].startswith( - "https://apihub.kma.go.kr/api/typ03/cgi/aws3/nph-awsm_tms_h06" - "?202305031000&1&108,419" + "https://apihub.kma.go.kr/api/typ03/cgi/aws3/nph-awsm_tms_h06?202305031000&1&108,419" ) assert call["url"].endswith("&_DT=RSW:AWSCHART&authKey=hub-key") -def test_generated_async_call_endpoint_appends_auth_key() -> None: +async def test_generated_async_call_endpoint_appends_auth_key() -> None: async def run() -> None: session = AsyncFakeSession(b"col1,col2\n1,2") - client = ApiHubGeneratedClient("hub-key", async_session=session) + client = ApiHubGeneratedClient("hub-key", session=session) - response = await client.acall_endpoint( - "kma_sfctm2", {"tm": "202605010900", "stn": "108"} - ) + response = await client.call_endpoint("kma_sfctm2", {"tm": "202605010900", "stn": "108"}) assert response.text == "col1,col2\n1,2" - assert session.calls[0]["url"] == ( - "https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php" - ) + assert session.calls[0]["url"] == ("https://apihub.kma.go.kr/api/typ01/url/kma_sfctm2.php") assert session.calls[0]["params"] == { "authKey": "hub-key", "tm": "202605010900", "stn": "108", } - asyncio.run(run()) + await run() -def test_generated_async_call_endpoint_can_use_sample_params() -> None: +async def test_generated_async_call_endpoint_can_use_sample_params() -> None: async def run() -> None: session = AsyncFakeSession() - client = ApiHubGeneratedClient("hub-key", async_session=session) + client = ApiHubGeneratedClient("hub-key", session=session) - await client.acall_endpoint("kma_sfctm2", use_sample=True, params={"stn": "108"}) + await client.call_endpoint("kma_sfctm2", use_sample=True, params={"stn": "108"}) assert session.calls[0]["params"] == { "authKey": "hub-key", @@ -150,48 +142,45 @@ async def run() -> None: "help": "1", } - asyncio.run(run()) + await run() -def test_generated_async_text_endpoint_parses_table() -> None: +async def test_generated_async_text_endpoint_parses_table() -> None: async def run() -> None: session = AsyncFakeSession(b"a b c\n1 2 3\n") - client = ApiHubGeneratedClient("hub-key", async_session=session) + client = ApiHubGeneratedClient("hub-key", session=session) - table = await client.atext_endpoint("kma_sfctm2", {"tm": "202605010900"}) + table = await client.text_endpoint("kma_sfctm2", {"tm": "202605010900"}) assert table.raw_lines - asyncio.run(run()) + await run() -def test_generated_async_call_endpoint_preserves_bare_query_order() -> None: +async def test_generated_async_call_endpoint_preserves_bare_query_order() -> None: async def run() -> None: session = AsyncFakeSession() - client = ApiHubGeneratedClient("hub-key", async_session=session) + client = ApiHubGeneratedClient("hub-key", session=session) - await client.acall_endpoint("aws3_nph_awsm_tms_h06", use_sample=True, params={"arg2": "1"}) + await client.call_endpoint("aws3_nph_awsm_tms_h06", use_sample=True, params={"arg2": "1"}) call = session.calls[0] assert call["params"] is None assert call["url"].endswith("&_DT=RSW:AWSCHART&authKey=hub-key") - asyncio.run(run()) + await run() -def test_generated_image_endpoint_returns_python_image_metadata() -> None: +async def test_generated_image_endpoint_returns_python_image_metadata() -> None: png = ( b"\x89PNG\r\n\x1a\n" b"\x00\x00\x00\r" - b"IHDR" - + (32).to_bytes(4, "big") - + (24).to_bytes(4, "big") - + b"\x08\x02\x00\x00\x00" + b"IHDR" + (32).to_bytes(4, "big") + (24).to_bytes(4, "big") + b"\x08\x02\x00\x00\x00" ) session = FakeSession(png) client = ApiHubGeneratedClient("hub-key", session=session) - image = client.image_endpoint("api_iwa_img_url_api_ret_grid_img", use_sample=True) + image = await client.image_endpoint("api_iwa_img_url_api_ret_grid_img", use_sample=True) assert image.format == "png" assert image.width == 32 diff --git a/tests/test_apihub_generator.py b/tests/test_apihub_generator.py index ec2a587..44a626b 100644 --- a/tests/test_apihub_generator.py +++ b/tests/test_apihub_generator.py @@ -23,7 +23,7 @@ class FakeSession: def __init__(self) -> None: self.calls: list[tuple[str, dict[str, Any]]] = [] - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> FakeResponse: self.calls.append((url, params)) if url.endswith("/apiList.do") and "seqApiSub" not in params: return FakeResponse('const apiList = [{"seqApi":238,"nmApi":"테스트서비스"}];') @@ -38,11 +38,11 @@ def get(self, url: str, *, params: dict[str, Any], timeout: float) -> FakeRespon raise AssertionError(f"unexpected URL: {url}") -def test_scrape_endpoints_keeps_api_list_when_generator_page_fails() -> None: +async def test_scrape_endpoints_keeps_api_list_when_generator_page_fails() -> None: original_categories = generator.CATEGORY_IDS generator.CATEGORY_IDS = (2,) try: - endpoints = generator.scrape_endpoints(FakeSession()) # type: ignore[arg-type] + endpoints = await generator.scrape_endpoints(FakeSession()) # type: ignore[arg-type] finally: generator.CATEGORY_IDS = original_categories diff --git a/tests/test_async_tps.py b/tests/test_async_tps.py new file mode 100644 index 0000000..dead978 --- /dev/null +++ b/tests/test_async_tps.py @@ -0,0 +1,281 @@ +"""공통 예산을 쓰는 실제 HTTPX 송신 경로와 비동기 공개 계약 회귀 검증.""" + +from __future__ import annotations + +import asyncio +import dataclasses +import inspect +from urllib.parse import quote + +import httpx +import pytest + +import kma +from kma import ApiHubClient, ApiHubGeneratedClient, AsyncTokenBucket, DataGoKrClient, KmaClient +from kma.apihub_endpoints import APIHUB_ATTACHMENTS, APIHUB_ENDPOINTS +from kma.exceptions import KmaAuthError, KmaError +from tools import update_apihub_endpoints as generator + + +class CountingBucket(AsyncTokenBucket): + def __init__(self): + super().__init__(10000) + self.count = 0 + + async def acquire(self): + await super().acquire() + self.count += 1 + + +def payload(*, page=1, total=1, items=None): + return { + "response": { + "header": {"resultCode": "00", "resultMsg": "NORMAL_SERVICE"}, + "body": { + "pageNo": page, + "numOfRows": 1, + "totalCount": total, + "items": {"item": [] if items is None else items}, + }, + } + } + + +async def test_shared_budget_covers_clients_debug_pagination_retry_and_redirect(monkeypatch): + budget = CountingBucket() + sent = [] + + async def no_sleep(_delay): + pass + + monkeypatch.setattr(kma._http.asyncio, "sleep", no_sleep) + + def handler(request): + sent.append((str(request.url), budget.count)) + if len(sent) == 1: + return httpx.Response(503) + if len(sent) == 2: + return httpx.Response(302, headers={"Location": "/resolved"}) + page = int(request.url.params.get("pageNo", 1)) + total = 2 if request.url.path.endswith("/pages") else 1 + return httpx.Response(200, json=payload(page=page, total=total)) + + async with httpx.AsyncClient( + transport=httpx.MockTransport(handler), follow_redirects=True + ) as session: + async with KmaClient("test-key", session=session, rate_limiter=budget) as typed: + await typed.now(nx=60, ny=127) + async with DataGoKrClient("test-key", session=session, rate_limiter=budget) as generic: + await generic.debug_fetch("S", "O") + pages = [page async for page in generic.iter_pages("S", "pages", num_of_rows=1)] + assert len(pages) == 2 + async with ApiHubClient("test-key", session=session, rate_limiter=budget) as hub: + await hub.request_path("/api/test") + async with ApiHubGeneratedClient( + "test-key", session=session, rate_limiter=budget + ) as generated: + await generated.call_endpoint(APIHUB_ENDPOINTS[0].name, use_sample=True) + await generated.debug_fetch_endpoint(APIHUB_ENDPOINTS[0], use_sample=True) + assert not session.is_closed + assert [count for _, count in sent] == list(range(1, 10)) + assert budget.count == 9 + + +@pytest.mark.parametrize("mutation", ["close", "auth", "cancel"]) +async def test_redirect_wait_rechecks_close_and_auth_and_honors_cancellation(mutation): + entered, release = asyncio.Event(), asyncio.Event() + + class WaitingBucket(CountingBucket): + async def acquire(self): + if self.count == 1: + entered.set() + await release.wait() + await super().acquire() + + sent = [] + + def handler(request): + sent.append(request) + return httpx.Response(302, headers={"Location": "/next"}) + + budget = WaitingBucket() + async with httpx.AsyncClient( + transport=httpx.MockTransport(handler), follow_redirects=True + ) as session: + client = ApiHubClient("test-key", session=session, rate_limiter=budget) + task = asyncio.create_task(client.request_path("/api/start")) + await asyncio.wait_for(entered.wait(), 2) + if mutation == "close": + await client.aclose() + error = RuntimeError + elif mutation == "auth": + session.auth = httpx.DigestAuth("user", "pass") + error = TypeError + else: + task.cancel() + error = asyncio.CancelledError + release.set() + with pytest.raises(error): + await task + await client.aclose() + assert not session.is_closed + assert len(sent) == 1 + if mutation == "cancel": + assert budget.count == 1 + + +@pytest.mark.parametrize( + "client_type", [KmaClient, DataGoKrClient, ApiHubClient, ApiHubGeneratedClient] +) +async def test_owned_lifecycle_sync_rejection_and_public_contract(client_type): + with httpx.Client() as sync: + with pytest.raises(TypeError, match="async"): + client_type("test-key", session=sync) + async with httpx.AsyncClient(auth=httpx.DigestAuth("u", "p")) as digest: + with pytest.raises(TypeError, match="unmetered"): + client_type("test-key", session=digest) + client = client_type("test-key") + assert client._session is None + assert client.rate_limiter.max_rps == 5 + session = client.session + assert isinstance(session, httpx.AsyncClient) + await client.aclose() + await client.aclose() + assert session.is_closed + with pytest.raises(RuntimeError, match="closed"): + async with client: + pass + assert not hasattr(client, "aio") and not hasattr(client, "close") + + +@pytest.mark.parametrize("status", [401, 403, 429, 503]) +async def test_status_retries_consume_one_token_per_attempt(status, monkeypatch): + budget = CountingBucket() + + async def no_sleep(_delay): + pass + + monkeypatch.setattr(kma._http.asyncio, "sleep", no_sleep) + async with httpx.AsyncClient( + transport=httpx.MockTransport(lambda request: httpx.Response(status)) + ) as session: + client = DataGoKrClient("test-key", session=session, rate_limiter=budget, retries=2) + with pytest.raises(KmaError) as error: + await client.request("S", "O") + assert error.value.status_code == status + assert budget.count == (1 if status in {401, 403} else 3) + + +async def test_public_errors_and_entire_debug_results_mask_actual_and_overridden_keys(): + secret, overridden = "key-secret/+abcdef", "override-secret/+uvwxyz" + echo = f"{secret} {quote(secret, safe='')} {overridden}" + async with httpx.AsyncClient( + transport=httpx.MockTransport( + lambda request: httpx.Response( + 200, json={"response": {"header": {"resultCode": "30", "resultMsg": echo}}} + ) + ) + ) as session: + client = DataGoKrClient(secret, session=session) + with pytest.raises(KmaAuthError) as captured: + await client.request("S", "O", {"serviceKey": overridden}) + assert secret not in repr(captured.value.args) + assert overridden not in repr(captured.value.metadata) + run = await client.debug_fetch("S", "O", {"serviceKey": overridden}) + assert secret not in repr(dataclasses.asdict(run)) + assert overridden not in repr(dataclasses.asdict(run)) + async with httpx.AsyncClient( + transport=httpx.MockTransport(lambda request: httpx.Response(200, text=echo)) + ) as session: + client = ApiHubGeneratedClient(secret, session=session) + run = await client.debug_fetch_endpoint(APIHUB_ENDPOINTS[0], use_sample=True) + assert secret not in repr(dataclasses.asdict(run)) + + +async def test_generator_reproduces_all_470_async_endpoint_calls(): + endpoint_fields = {field.name for field in dataclasses.fields(generator.Endpoint)} - { + "examples" + } + attachment_fields = {field.name for field in dataclasses.fields(generator.Attachment)} + endpoints = [ + generator.Endpoint(**{key: getattr(spec, key) for key in endpoint_fields}) + for spec in APIHUB_ENDPOINTS + ] + attachments = [ + generator.Attachment(**{key: getattr(spec, key) for key in attachment_fields}) + for spec in APIHUB_ATTACHMENTS + ] + code = generator.render_module(endpoints, attachments) + namespace = {"__name__": "kma._generated_test", "__package__": "kma"} + exec(compile(code, "", "exec"), namespace) + generated_type = namespace["ApiHubGeneratedClient"] + budget = CountingBucket() + async with httpx.AsyncClient( + transport=httpx.MockTransport(lambda request: httpx.Response(200, text="col\nvalue")) + ) as session: + client = generated_type("test-key", session=session, rate_limiter=budget) + for spec in APIHUB_ENDPOINTS: + assert inspect.iscoroutinefunction(getattr(ApiHubGeneratedClient, spec.name)) + method = getattr(client, spec.name) + assert inspect.iscoroutinefunction(method) + params = {key: spec.sample_params.get(key, "0") for key in spec.parameters} + response = await method(**params) + assert response.status_code == 200 + assert len(APIHUB_ENDPOINTS) == budget.count == 470 + + +@pytest.mark.parametrize("method", ["forecast_short", "forecast"]) +async def test_forecast_parse_errors_protect_echoed_keys(method): + secret = "synthetic/key+value" + async with httpx.AsyncClient( + transport=httpx.MockTransport( + lambda request: httpx.Response( + 200, + json=payload( + items=[ + { + "category": "TMP", + "fcstValue": secret, + } + ] + ), + ) + ) + ) as session: + client = KmaClient(secret, session=session) + with pytest.raises(KmaError) as captured: + await getattr(client, method)(nx=60, ny=127) + assert secret not in str(captured.value) + assert secret not in repr(captured.value.args) + + +async def test_open_api_error_and_custom_credential_override_are_protected(caplog): + secret = "overridden/key+value" + caplog.set_level("INFO", logger="httpx") + async with httpx.AsyncClient( + transport=httpx.MockTransport( + lambda request: httpx.Response( + 200, + json={ + "response": { + "header": { + "resultCode": "30", + "resultMsg": secret, + } + } + }, + ) + ) + ) as session: + hub = ApiHubClient(secret, session=session) + with pytest.raises(KmaAuthError) as captured: + await hub.open_api("S", "O") + assert secret not in str(captured.value) + client = DataGoKrClient("configured-key", session=session, service_key_param="customKey") + with pytest.raises(KmaAuthError) as captured: + await client.request("S", "O", {"customKey": secret}) + assert secret not in str(captured.value) + run = await client.debug_fetch("S", "O", {"customKey": secret}) + assert secret not in str(run) + assert secret not in caplog.text + assert quote(secret, safe="") not in caplog.text diff --git a/tests/test_cli.py b/tests/test_cli.py index ad27e00..edb6e23 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,6 +37,12 @@ class FakeForecast: class FakeClient: + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + last_init_key: str | None = None last_from_env_called = False last_call: tuple[str, dict[str, Any]] | None = None @@ -50,15 +56,15 @@ def from_env(cls) -> FakeClient: cls.last_from_env_called = True return cls("env-key") - def now(self, **kwargs: Any) -> FakeSnapshot: + async def now(self, **kwargs: Any) -> FakeSnapshot: FakeClient.last_call = ("now", kwargs) return FakeSnapshot(datetime(2026, 4, 30, 14, 0, tzinfo=KST), 60, 127, 18.4) - def forecast(self, **kwargs: Any) -> list[FakeForecast]: + async def forecast(self, **kwargs: Any) -> list[FakeForecast]: FakeClient.last_call = ("forecast", kwargs) return [FakeForecast(datetime(2026, 4, 30, 15, 0, tzinfo=KST), 60, 127, "TMP", 18.4)] - def forecast_short(self, **kwargs: Any) -> list[FakeForecast]: + async def forecast_short(self, **kwargs: Any) -> list[FakeForecast]: FakeClient.last_call = ("forecast_short", kwargs) return [FakeForecast(datetime(2026, 4, 30, 15, 0, tzinfo=KST), 60, 127, "T1H", 18.4)] @@ -68,6 +74,12 @@ class FakeApiHubResponse: class FakeApiHubClient: + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + last_init_key: str | None = None last_from_env_called = False last_call: tuple[str, dict[str, str]] | None = None @@ -80,7 +92,7 @@ def from_env(cls) -> FakeApiHubClient: cls.last_from_env_called = True return cls("env-hub-key") - def request_path(self, path: str, params: dict[str, str]) -> FakeApiHubResponse: + async def request_path(self, path: str, params: dict[str, str]) -> FakeApiHubResponse: FakeApiHubClient.last_call = (path, params) return FakeApiHubResponse() @@ -110,9 +122,7 @@ def test_cli_forecast_short_uses_from_env_and_latlon() -> None: stream = io.StringIO() try: with redirect_stdout(stream): - result = cli.main( - ["forecast", "--short", "--lat", "37.5665", "--lon", "126.9780"] - ) + result = cli.main(["forecast", "--short", "--lat", "37.5665", "--lon", "126.9780"]) finally: cli.KmaClient = original @@ -177,9 +187,7 @@ def test_cli_apihub_param_value_keeps_extra_equals() -> None: stream = io.StringIO() try: with redirect_stdout(stream): - result = cli.main( - ["apihub", "/api/typ01/url/x.php", "--param", "q=a=b&c"] - ) + result = cli.main(["apihub", "/api/typ01/url/x.php", "--param", "q=a=b&c"]) finally: cli.ApiHubClient = original @@ -194,9 +202,7 @@ def test_cli_apihub_uses_explicit_auth_key() -> None: stream = io.StringIO() try: with redirect_stdout(stream): - result = cli.main( - ["apihub", "/api/typ01/url/x.php", "--auth-key", "explicit-hub"] - ) + result = cli.main(["apihub", "/api/typ01/url/x.php", "--auth-key", "explicit-hub"]) finally: cli.ApiHubClient = original diff --git a/tests/test_client.py b/tests/test_client.py index 23cbd90..a3cdb4a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,6 +1,6 @@ from __future__ import annotations -import asyncio +import inspect from datetime import datetime, timezone from typing import Any, Callable, TypeVar @@ -42,7 +42,7 @@ def json(self) -> dict[str, Any]: class XmlErrorSession: - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlErrorResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlErrorResponse: del url, params, timeout return XmlErrorResponse() @@ -54,7 +54,7 @@ class XmlNoDataResponse(XmlErrorResponse): class XmlNoDataSession(XmlErrorSession): - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlNoDataResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlNoDataResponse: del url, params, timeout return XmlNoDataResponse() @@ -76,7 +76,7 @@ def last_url(self) -> str | None: return None return self.calls[-1]["url"] - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> FakeResponse: self.calls.append({"url": url, "params": params, "timeout": timeout}) return FakeResponse(self.payload) @@ -95,9 +95,11 @@ async def aclose(self) -> None: self.closed = True -def assert_raises(exc_type: type[T], func: Callable[[], object]) -> T: +async def assert_raises(exc_type: type[T], func: Callable[[], object]) -> T: try: - func() + result = func() + if inspect.isawaitable(result): + await result except exc_type as exc: return exc except Exception as exc: # pragma: no cover - failure path for clearer direct-run output @@ -123,7 +125,7 @@ def _error_payload(code: str, message: str = "ERROR") -> dict[str, Any]: } -def test_now_pivots_observed_items() -> None: +async def test_now_pivots_observed_items() -> None: session = FakeSession( _payload( [ @@ -138,7 +140,7 @@ def test_now_pivots_observed_items() -> None: ) client = KmaClient("decoded-key", session=session) - snapshot = client.now(nx=60, ny=127, when=datetime(2026, 4, 30, 14, 45, tzinfo=KST)) + snapshot = await client.now(nx=60, ny=127, when=datetime(2026, 4, 30, 14, 45, tzinfo=KST)) assert snapshot.observed_at.isoformat() == "2026-04-30T14:00:00+09:00" assert snapshot.temperature == 18.4 @@ -161,7 +163,7 @@ def test_now_pivots_observed_items() -> None: assert session.last_params["dataType"] == "JSON" -def test_forecast_service_matches_krheritage_style_facade() -> None: +async def test_forecast_service_matches_krheritage_style_facade() -> None: session = FakeSession( _payload( { @@ -178,22 +180,21 @@ def test_forecast_service_matches_krheritage_style_facade() -> None: ) client = KmaClient("decoded-key", session=session) - items = client.forecast.vilage( + items = await client.forecast.vilage( nx=60, ny=127, when=datetime(2026, 4, 30, 14, 15, tzinfo=KST), ) assert items[0].value == 18.4 - assert ( - client.forecast(nx=60, ny=127, when=datetime(2026, 4, 30, 14, 15, tzinfo=KST))[0].value - == 18.4 - ) + assert (await client.forecast(nx=60, ny=127, when=datetime(2026, 4, 30, 14, 15, tzinfo=KST)))[ + 0 + ].value == 18.4 assert session.last_params is not None assert session.last_params["base_time"] == "1400" -def test_aio_client_exposes_async_forecast_service() -> None: +async def test_aio_client_exposes_async_forecast_service() -> None: async def run() -> None: session = AsyncFakeSession( _payload( @@ -205,7 +206,7 @@ async def run() -> None: ) ) - async with KmaClient.aio("decoded-key", async_session=session) as client: + async with KmaClient("decoded-key", session=session) as client: snapshot = await client.forecast.now( nx=60, ny=127, @@ -217,20 +218,20 @@ async def run() -> None: assert session.closed is False assert client.closed is True - asyncio.run(run()) + await run() -def test_client_service_key_strips_copied_whitespace() -> None: +async def test_client_service_key_strips_copied_whitespace() -> None: session = FakeSession(_payload({"version": "202604301400"})) client = KmaClient(" decoded \n key\t", session=session) - client.version("ODAM", when=datetime(2026, 4, 30, 14, 0, tzinfo=KST)) + (await client.version("ODAM", when=datetime(2026, 4, 30, 14, 0, tzinfo=KST))) assert session.last_params is not None assert session.last_params["serviceKey"] == "decodedkey" -def test_forecast_uses_latlon_conversion_and_preserves_pcp_labels() -> None: +async def test_forecast_uses_latlon_conversion_and_preserves_pcp_labels() -> None: session = FakeSession( _payload( [ @@ -269,7 +270,7 @@ def test_forecast_uses_latlon_conversion_and_preserves_pcp_labels() -> None: ) client = KmaClient("decoded-key", session=session) - items = client.forecast( + items = await client.forecast( lat=37.5665, lon=126.9780, when=datetime(2026, 4, 30, 14, 15, tzinfo=KST), @@ -287,7 +288,7 @@ def test_forecast_uses_latlon_conversion_and_preserves_pcp_labels() -> None: assert items[2].label == "맑음" -def test_client_accepts_standard_location_objects_and_returns_category_enums() -> None: +async def test_client_accepts_standard_location_objects_and_returns_category_enums() -> None: session = FakeSession( _payload( [ @@ -306,7 +307,7 @@ def test_client_accepts_standard_location_objects_and_returns_category_enums() - ) client = KmaClient("decoded-key", session=session) - items = client.forecast( + items = await client.forecast( location=LatLon(37.5665, 126.9780), when=datetime(2026, 4, 30, 14, 15, tzinfo=KST), ) @@ -322,7 +323,7 @@ def test_client_accepts_standard_location_objects_and_returns_category_enums() - assert isinstance(items[0].latlon, LatLon) -def test_client_accepts_grid_location_mapping() -> None: +async def test_client_accepts_grid_location_mapping() -> None: session = FakeSession( _payload( [ @@ -333,7 +334,7 @@ def test_client_accepts_grid_location_mapping() -> None: ) client = KmaClient("decoded-key", session=session) - snapshot = client.now( + snapshot = await client.now( location={"nx": "60", "ny": "127"}, when=datetime(2026, 4, 30, 14, 45, tzinfo=KST), ) @@ -343,7 +344,7 @@ def test_client_accepts_grid_location_mapping() -> None: assert snapshot.temperature == 18.4 -def test_fetch_items_accepts_single_item_dict() -> None: +async def test_fetch_items_accepts_single_item_dict() -> None: session = FakeSession( _payload( { @@ -360,13 +361,15 @@ def test_fetch_items_accepts_single_item_dict() -> None: ) client = KmaClient("decoded-key", session=session) - items = client.forecast_short(nx=60, ny=127, when=datetime(2026, 4, 30, 14, 50, tzinfo=KST)) + items = await client.forecast_short( + nx=60, ny=127, when=datetime(2026, 4, 30, 14, 50, tzinfo=KST) + ) assert len(items) == 1 assert items[0].label == "소나기" -def test_version_converts_aware_datetime_to_kst() -> None: +async def test_version_converts_aware_datetime_to_kst() -> None: session = FakeSession( { "response": { @@ -377,7 +380,7 @@ def test_version_converts_aware_datetime_to_kst() -> None: ) client = KmaClient("decoded-key", session=session) - client.version("ODAM", datetime(2026, 4, 30, 5, 30, tzinfo=timezone.utc)) + (await client.version("ODAM", datetime(2026, 4, 30, 5, 30, tzinfo=timezone.utc))) assert session.last_url is not None assert session.last_url.endswith("/getFcstVersion") @@ -385,18 +388,18 @@ def test_version_converts_aware_datetime_to_kst() -> None: assert session.last_params["basedatetime"] == "202604301430" -def test_coordinate_validation_rejects_partial_mixed_and_out_of_range_inputs() -> None: +async def test_coordinate_validation_rejects_partial_mixed_and_out_of_range_inputs() -> None: client = KmaClient("decoded-key", session=FakeSession(_payload([]))) - assert_raises(ValueError, lambda: client.now(lat=37.5)) - assert_raises(ValueError, lambda: client.now(nx=60)) - assert_raises(ValueError, lambda: client.now(lat=37.5, lon=127.0, nx=60, ny=127)) - assert_raises(ValueError, lambda: client.now(lat=91.0, lon=127.0)) - assert_raises(ValueError, lambda: client.now(nx=0, ny=127)) - assert_raises(ValueError, lambda: client.now(nx=60, ny=254)) + (await assert_raises(ValueError, lambda: client.now(lat=37.5))) + (await assert_raises(ValueError, lambda: client.now(nx=60))) + (await assert_raises(ValueError, lambda: client.now(lat=37.5, lon=127.0, nx=60, ny=127))) + (await assert_raises(ValueError, lambda: client.now(lat=91.0, lon=127.0))) + (await assert_raises(ValueError, lambda: client.now(nx=0, ny=127))) + (await assert_raises(ValueError, lambda: client.now(nx=60, ny=254))) -def test_result_codes_raise_typed_exceptions() -> None: +async def test_result_codes_raise_typed_exceptions() -> None: auth_codes = {"20", "30", "31"} server_codes = {"04", "99"} # `22`는 quota라 아래에서 따로 본다. 예전에는 `12`와 한 묶음이었고 @@ -406,20 +409,22 @@ def test_result_codes_raise_typed_exceptions() -> None: for code in auth_codes: client = KmaClient("bad-key", session=FakeSession(_error_payload(code))) - error = assert_raises(KmaAuthError, lambda client=client: client.now(nx=60, ny=127)) + error = await assert_raises(KmaAuthError, lambda client=client: client.now(nx=60, ny=127)) assert error.failure_kind == "auth" assert error.result_code == code assert error.retryable is False for code in server_codes: client = KmaClient("decoded-key", session=FakeSession(_error_payload(code))) - error = assert_raises(KmaServerError, lambda client=client: client.now(nx=60, ny=127)) + error = await assert_raises(KmaServerError, lambda client=client: client.now(nx=60, ny=127)) assert error.failure_kind == "server" assert error.retryable is True for code in request_codes: client = KmaClient("decoded-key", session=FakeSession(_error_payload(code))) - error = assert_raises(KmaRequestError, lambda client=client: client.now(nx=60, ny=127)) + error = await assert_raises( + KmaRequestError, lambda client=client: client.now(nx=60, ny=127) + ) assert error.provider == "data.go.kr" assert error.endpoint == "getUltraSrtNcst" assert error.failure_kind == "request" @@ -428,19 +433,17 @@ def test_result_codes_raise_typed_exceptions() -> None: # 일일 quota 초과. 한도는 자정에 리셋되므로 **당일 재시도는 성공할 수 없다** — # `retryable=True`면 호출자가 성공 못 할 것에 retry budget을 태운다. quota_client = KmaClient("decoded-key", session=FakeSession(_error_payload("22"))) - quota_error = assert_raises( - KmaRequestError, lambda: quota_client.now(nx=60, ny=127) - ) + quota_error = await assert_raises(KmaRequestError, lambda: quota_client.now(nx=60, ny=127)) assert quota_error.result_code == "22" assert quota_error.failure_kind == "quota" assert quota_error.retryable is False -def test_http_200_xml_quota_envelope_preserves_nonretryable_classification() -> None: +async def test_http_200_xml_quota_envelope_preserves_nonretryable_classification() -> None: """JSON 요청에도 gateway 오류는 XML 200으로 와서 JSON 분류를 우회할 수 있다.""" client = KmaClient("decoded-key", session=XmlErrorSession()) - error = assert_raises( + error = await assert_raises( KmaRequestError, lambda: client.now(nx=60, ny=127), ) @@ -450,10 +453,10 @@ def test_http_200_xml_quota_envelope_preserves_nonretryable_classification() -> assert error.retryable is False -def test_http_200_xml_no_data_envelope_returns_empty_forecast() -> None: +async def test_http_200_xml_no_data_envelope_returns_empty_forecast() -> None: client = KmaClient("decoded-key", session=XmlNoDataSession()) - items = client.forecast_short( + items = await client.forecast_short( nx=60, ny=127, when=datetime(2026, 4, 30, 14, 50, tzinfo=KST), @@ -462,10 +465,10 @@ def test_http_200_xml_no_data_envelope_returns_empty_forecast() -> None: assert items == [] -def test_no_data_result_code_returns_empty_forecast() -> None: +async def test_no_data_result_code_returns_empty_forecast() -> None: client = KmaClient("decoded-key", session=FakeSession(_error_payload("03", "NO_DATA"))) - items = client.forecast_short( + items = await client.forecast_short( nx=60, ny=127, when=datetime(2026, 4, 30, 14, 50, tzinfo=KST), @@ -474,23 +477,23 @@ def test_no_data_result_code_returns_empty_forecast() -> None: assert items == [] -def test_no_data_result_code_returns_empty_snapshot() -> None: +async def test_no_data_result_code_returns_empty_snapshot() -> None: client = KmaClient("decoded-key", session=FakeSession(_error_payload("03", "NO_DATA"))) - snapshot = client.now(nx=60, ny=127, when=datetime(2026, 4, 30, 14, 45, tzinfo=KST)) + snapshot = await client.now(nx=60, ny=127, when=datetime(2026, 4, 30, 14, 45, tzinfo=KST)) assert snapshot.temperature is None assert snapshot.humidity is None assert snapshot.raw["items"] == [] -def test_malformed_envelope_raises_parse_error() -> None: +async def test_malformed_envelope_raises_parse_error() -> None: client = KmaClient("decoded-key", session=FakeSession({"not_response": {}})) - assert_raises(KmaParseError, lambda: client.now(nx=60, ny=127)) + (await assert_raises(KmaParseError, lambda: client.now(nx=60, ny=127))) -def test_missing_items_raises_parse_error() -> None: +async def test_missing_items_raises_parse_error() -> None: client = KmaClient( "decoded-key", session=FakeSession( @@ -503,10 +506,10 @@ def test_missing_items_raises_parse_error() -> None: ), ) - assert_raises(KmaParseError, lambda: client.now(nx=60, ny=127)) + (await assert_raises(KmaParseError, lambda: client.now(nx=60, ny=127))) -def test_malformed_forecast_item_raises_parse_error() -> None: +async def test_malformed_forecast_item_raises_parse_error() -> None: client = KmaClient( "decoded-key", session=FakeSession( @@ -526,4 +529,4 @@ def test_malformed_forecast_item_raises_parse_error() -> None: ), ) - assert_raises(KmaParseError, lambda: client.forecast(nx=60, ny=127)) + (await assert_raises(KmaParseError, lambda: client.forecast(nx=60, ny=127))) diff --git a/tests/test_datagokr.py b/tests/test_datagokr.py index 8b37f44..a10bd55 100644 --- a/tests/test_datagokr.py +++ b/tests/test_datagokr.py @@ -1,9 +1,11 @@ from __future__ import annotations -import asyncio +import inspect from datetime import datetime from typing import Any, Callable +import pytest + from kma import ( KMA_DATA_GOKR_DATASETS, ApiCatalogEntry, @@ -20,9 +22,10 @@ next_page_no, sanitize_request_params, ) -from kma.datagokr import AsyncDataGoKrClient, DataGoKrClient +from kma.datagokr import DataGoKrClient from kma.exceptions import KmaAuthError, KmaParseError, KmaRequestError, KmaServerError from kma.metadata import redact_credentials_in_text +from kma.pagination import PaginationLimitWarning from kma.time_utils import KST @@ -54,15 +57,13 @@ def json(self) -> dict[str, Any]: class XmlErrorSession: - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlErrorResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlErrorResponse: del url, params, timeout return XmlErrorResponse() class AsyncXmlErrorSession: - async def get( - self, url: str, *, params: dict[str, Any], timeout: float - ) -> XmlErrorResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlErrorResponse: del url, params, timeout return XmlErrorResponse() @@ -74,15 +75,13 @@ class XmlNoDataResponse(XmlErrorResponse): class XmlNoDataSession(XmlErrorSession): - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlNoDataResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlNoDataResponse: del url, params, timeout return XmlNoDataResponse() class AsyncXmlNoDataSession(AsyncXmlErrorSession): - async def get( - self, url: str, *, params: dict[str, Any], timeout: float - ) -> XmlNoDataResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> XmlNoDataResponse: del url, params, timeout return XmlNoDataResponse() @@ -96,7 +95,9 @@ class ArbitraryXmlSession(XmlErrorSession): def __init__(self, code: str) -> None: self.code = code - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> ArbitraryXmlResponse: + async def get( + self, url: str, *, params: dict[str, Any], timeout: float + ) -> ArbitraryXmlResponse: del url, params, timeout return ArbitraryXmlResponse(self.code) @@ -106,7 +107,7 @@ def __init__(self, payload: dict[str, Any]) -> None: self.payload = payload self.calls: list[dict[str, Any]] = [] - def get(self, url: str, *, params: dict[str, Any], timeout: float) -> FakeResponse: + async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> FakeResponse: self.calls.append({"url": url, "params": params, "timeout": timeout}) return FakeResponse(self.payload) @@ -121,9 +122,11 @@ async def get(self, url: str, *, params: dict[str, Any], timeout: float) -> Fake return FakeResponse(self.payload) -def assert_raises(exc_type: type[BaseException], func: Callable[[], object]) -> None: +async def assert_raises(exc_type: type[BaseException], func: Callable[[], object]) -> None: try: - func() + result = func() + if inspect.isawaitable(result): + await result except exc_type: return raise AssertionError(f"expected {exc_type.__name__}") @@ -157,29 +160,31 @@ def _no_data_payload() -> dict[str, Any]: } -def test_datagokr_generic_request_builds_service_operation_url() -> None: +async def test_datagokr_generic_request_builds_service_operation_url() -> None: session = FakeSession(_payload([{"wfSv": "맑음"}])) client = DataGoKrClient("decoded-key", session=session) - body = client.request( + body = await client.request( "MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}, ) assert body["items"]["item"][0]["wfSv"] == "맑음" - assert session.calls[0]["url"] == "https://apis.data.go.kr/1360000/MidFcstInfoService/getMidFcst" + assert ( + session.calls[0]["url"] == "https://apis.data.go.kr/1360000/MidFcstInfoService/getMidFcst" + ) assert session.calls[0]["params"]["serviceKey"] == "decoded-key" assert session.calls[0]["params"]["dataType"] == "JSON" assert session.calls[0]["params"]["pageNo"] == 1 assert session.calls[0]["params"]["numOfRows"] == 10 -def test_datagokr_http_200_xml_quota_is_nonretryable() -> None: +async def test_datagokr_http_200_xml_quota_is_nonretryable() -> None: client = DataGoKrClient("decoded-key", session=XmlErrorSession()) try: - client.request("MidFcstInfoService", "getMidFcst") + (await client.request("MidFcstInfoService", "getMidFcst")) except KmaRequestError as error: assert error.result_code == "22" assert error.failure_kind == "quota" @@ -188,33 +193,33 @@ def test_datagokr_http_200_xml_quota_is_nonretryable() -> None: raise AssertionError("expected KmaRequestError") -def test_datagokr_http_200_xml_no_data_is_empty_success() -> None: +async def test_datagokr_http_200_xml_no_data_is_empty_success() -> None: client = DataGoKrClient("decoded-key", session=XmlNoDataSession()) - body = client.request("MidFcstInfoService", "getMidFcst") + body = await client.request("MidFcstInfoService", "getMidFcst") assert body["items"]["item"] == [] assert body["totalCount"] == 0 -def test_datagokr_unrelated_xml_never_becomes_empty_or_typed_success() -> None: +async def test_datagokr_unrelated_xml_never_becomes_empty_or_typed_success() -> None: for code in ("03", "22"): client = DataGoKrClient("decoded-key", session=ArbitraryXmlSession(code)) try: - client.request("MidFcstInfoService", "getMidFcst") + (await client.request("MidFcstInfoService", "getMidFcst")) except KmaParseError: pass else: # pragma: no cover - 실패 메시지 명확화 raise AssertionError(f"unrelated XML code {code} must remain a parse error") -def test_datagokr_async_request_builds_service_operation_url() -> None: +async def test_datagokr_async_request_builds_service_operation_url() -> None: async def run() -> None: session = AsyncFakeSession(_payload([{"wfSv": "맑음"}])) - client = DataGoKrClient("decoded-key", async_session=session) + client = DataGoKrClient("decoded-key", session=session) - body = await client.arequest( + body = await client.request( "MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}, @@ -226,15 +231,15 @@ async def run() -> None: ) assert session.calls[0]["params"]["serviceKey"] == "decoded-key" - asyncio.run(run()) + await run() -def test_datagokr_async_http_200_xml_quota_is_nonretryable() -> None: +async def test_datagokr_async_http_200_xml_quota_is_nonretryable() -> None: async def run() -> None: - client = DataGoKrClient("decoded-key", async_session=AsyncXmlErrorSession()) + client = DataGoKrClient("decoded-key", session=AsyncXmlErrorSession()) try: - await client.arequest("MidFcstInfoService", "getMidFcst") + await client.request("MidFcstInfoService", "getMidFcst") except KmaRequestError as error: assert error.result_code == "22" assert error.failure_kind == "quota" @@ -242,27 +247,27 @@ async def run() -> None: else: # pragma: no cover - 실패 메시지 명확화 raise AssertionError("expected KmaRequestError") - asyncio.run(run()) + await run() -def test_datagokr_async_http_200_xml_no_data_is_empty_success() -> None: +async def test_datagokr_async_http_200_xml_no_data_is_empty_success() -> None: async def run() -> None: - client = DataGoKrClient("decoded-key", async_session=AsyncXmlNoDataSession()) + client = DataGoKrClient("decoded-key", session=AsyncXmlNoDataSession()) - body = await client.arequest("MidFcstInfoService", "getMidFcst") + body = await client.request("MidFcstInfoService", "getMidFcst") assert body["items"]["item"] == [] assert body["totalCount"] == 0 - asyncio.run(run()) + await run() -def test_datagokr_aio_returns_async_facade() -> None: +async def test_datagokr_aio_returns_async_facade() -> None: async def run() -> None: session = AsyncFakeSession(_payload([{"wfSv": "맑음"}])) - client = DataGoKrClient.aio("decoded-key", async_session=session) + client = DataGoKrClient("decoded-key", session=session) - assert isinstance(client, AsyncDataGoKrClient) + assert isinstance(client, DataGoKrClient) assert client.service_key == "decoded-key" async with client: @@ -278,23 +283,23 @@ async def run() -> None: assert session.calls[0]["params"]["serviceKey"] == "decoded-key" assert client.closed is True - asyncio.run(run()) + await run() -def test_datagokr_service_key_strips_copied_whitespace() -> None: +async def test_datagokr_service_key_strips_copied_whitespace() -> None: session = FakeSession(_payload([{"wfSv": "맑음"}])) client = DataGoKrClient(" decoded \n key\t", session=session) - client.request("MidFcstInfoService", "getMidFcst") + (await client.request("MidFcstInfoService", "getMidFcst")) assert session.calls[0]["params"]["serviceKey"] == "decodedkey" -def test_datagokr_request_with_metadata_sanitizes_service_key() -> None: +async def test_datagokr_request_with_metadata_sanitizes_service_key() -> None: session = FakeSession(_payload([{"wfSv": "맑음"}])) client = DataGoKrClient("decoded-key", session=session) - body, metadata = client.request_with_metadata( + body, metadata = await client.request_with_metadata( "MidFcstInfoService", "getMidFcst", {"stnId": "108", "tmFc": "202605010600"}, @@ -307,20 +312,20 @@ def test_datagokr_request_with_metadata_sanitizes_service_key() -> None: assert "serviceKey" not in metadata.request_params -def test_datagokr_service_key_parameter_name_is_configurable() -> None: +async def test_datagokr_service_key_parameter_name_is_configurable() -> None: session = FakeSession(_payload([{"wfSv": "맑음"}])) client = DataGoKrClient("decoded-key", service_key_param="ServiceKey", session=session) - client.request("MidFcstInfoService", "getMidFcst") + (await client.request("MidFcstInfoService", "getMidFcst")) assert session.calls[0]["params"]["ServiceKey"] == "decoded-key" assert "serviceKey" not in session.calls[0]["params"] -def test_datagokr_items_wraps_single_item_dict() -> None: +async def test_datagokr_items_wraps_single_item_dict() -> None: client = DataGoKrClient("decoded-key", session=FakeSession(_payload({"wfSv": "맑음"}))) - assert client.items("MidFcstInfoService", "getMidFcst")[0] == {"wfSv": "맑음"} + assert (await client.items("MidFcstInfoService", "getMidFcst"))[0] == {"wfSv": "맑음"} def test_datagokr_dataset_catalog_is_kma_only() -> None: @@ -333,8 +338,7 @@ def test_datagokr_dataset_catalog_is_kma_only() -> None: assert sum(1 for dataset in datasets if dataset.gateway == "datagokr") == 38 assert sum(1 for dataset in datasets if dataset.gateway == "apihub") == 48 assert ( - sum(len(dataset.operations) for dataset in datasets if dataset.gateway == "datagokr") - == 160 + sum(len(dataset.operations) for dataset in datasets if dataset.gateway == "datagokr") == 160 ) assert all(dataset.title.startswith("\uae30\uc0c1\uccad") for dataset in datasets) assert client.dataset("15084084").operations == ( @@ -435,11 +439,11 @@ def test_env_loader_supports_source_specific_keys_and_local_dotenv( assert DataGoKrClient.from_env(session=FakeSession(_payload([]))).service_key == "datagokrkey" -def test_datagokr_dataset_catalog_request_by_id() -> None: +async def test_datagokr_dataset_catalog_request_by_id() -> None: session = FakeSession(_payload({"stnId": "108", "tm": "2026-05-01"})) client = DataGoKrClient("decoded-key", session=session) - rows = client.dataset_items( + rows = await client.dataset_items( "15059093", { "startDt": "20260501", @@ -458,13 +462,13 @@ def test_datagokr_dataset_catalog_request_by_id() -> None: assert rows[0].raw["stnId"] == "108" -def test_datagokr_dataset_catalog_multi_operation_requires_selection() -> None: +async def test_datagokr_dataset_catalog_multi_operation_requires_selection() -> None: session = FakeSession(_payload({"beachNum": "1", "tm": "202205011600", "wh": "0.7"})) client = DataGoKrClient("decoded-key", session=session) - assert_raises(ValueError, lambda: client.dataset_items("15102239", {"beach_num": "1"})) + (await assert_raises(ValueError, lambda: client.dataset_items("15102239", {"beach_num": "1"}))) - rows = client.dataset_items( + rows = await client.dataset_items( "15102239", {"beach_num": "1", "searchTime": "202205011600"}, operation="getWhBuoyBeach", @@ -477,15 +481,15 @@ def test_datagokr_dataset_catalog_multi_operation_requires_selection() -> None: assert rows[0].operation == "getWhBuoyBeach" -def test_datagokr_dataset_catalog_rejects_api_hub_linked_entries() -> None: +async def test_datagokr_dataset_catalog_rejects_api_hub_linked_entries() -> None: client = DataGoKrClient("decoded-key", session=FakeSession(_payload([]))) assert client.dataset("15139470").gateway == "apihub" - assert_raises(ValueError, lambda: client.dataset_items("15139470")) - assert_raises(ValueError, lambda: client.dataset("99999999")) + (await assert_raises(ValueError, lambda: client.dataset_items("15139470"))) + (await assert_raises(ValueError, lambda: client.dataset("99999999"))) -def test_datagokr_pagination_helpers_and_iter_pages_guard() -> None: +async def test_datagokr_pagination_helpers_and_iter_pages_guard() -> None: first = _paged_payload([{"id": 1}], page_no=1, total_count=3) second = _paged_payload([{"id": 2}], page_no=2, total_count=3) third = _paged_payload([{"id": 3}], page_no=3, total_count=3) @@ -498,12 +502,10 @@ def test_datagokr_pagination_helpers_and_iter_pages_guard() -> None: session.payload = first pages = [] - for index, body in enumerate( - client.iter_pages("S", "O", num_of_rows=1, max_pages=2), - start=1, - ): - pages.append(body) - session.payload = second if index == 1 else third + with pytest.warns(PaginationLimitWarning): + async for body in client.iter_pages("S", "O", num_of_rows=1, max_pages=2): + pages.append(body) + session.payload = second if len(pages) == 1 else third assert [page["pageNo"] for page in pages] == [1, 2] @@ -541,7 +543,7 @@ def test_sanitized_params_and_cache_key_ignore_credentials() -> None: assert left != changed_grid -def test_mid_forecast_helpers_do_not_guess_reg_id_mapping() -> None: +async def test_mid_forecast_helpers_do_not_guess_reg_id_mapping() -> None: session = FakeSession( _payload( { @@ -553,7 +555,7 @@ def test_mid_forecast_helpers_do_not_guess_reg_id_mapping() -> None: ) client = DataGoKrClient("decoded-key", session=session) - rows = client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600") + rows = await client.mid_land_forecast(reg_id="11B00000", tm_fc="202605010600") assert rows[0].operation == "getMidLandFcst" assert rows[0].reg_id == "11B00000" @@ -564,7 +566,7 @@ def test_mid_forecast_helpers_do_not_guess_reg_id_mapping() -> None: assert "ny" not in rows[0].metadata.request_params -def test_mid_forecast_helpers_can_select_latest_tm_fc() -> None: +async def test_mid_forecast_helpers_can_select_latest_tm_fc() -> None: session = FakeSession( _payload( { @@ -576,20 +578,22 @@ def test_mid_forecast_helpers_can_select_latest_tm_fc() -> None: ) client = DataGoKrClient("decoded-key", session=session) - rows = client.mid_land_forecast( + rows = await client.mid_land_forecast( reg_id="11B00000", when=datetime(2026, 5, 1, 6, 5, tzinfo=KST), ) assert session.calls[0]["params"]["tmFc"] == "202604301800" assert rows[0].tm_fc == "202604301800" - assert_raises( - ValueError, - lambda: client.mid_forecast(stn_id=108, tm_fc="202605010600", when=datetime.now(KST)), + ( + await assert_raises( + ValueError, + lambda: client.mid_forecast(stn_id=108, tm_fc="202605010600", when=datetime.now(KST)), + ) ) -def test_datagokr_mid_sea_forecast_helper() -> None: +async def test_datagokr_mid_sea_forecast_helper() -> None: session = FakeSession( _payload( { @@ -601,14 +605,17 @@ def test_datagokr_mid_sea_forecast_helper() -> None: ) client = DataGoKrClient("decoded-key", session=session) - rows = client.mid_sea_forecast(reg_id="12A20000", tm_fc="202605010600") + rows = await client.mid_sea_forecast(reg_id="12A20000", tm_fc="202605010600") assert rows[0].operation == "getMidSeaFcst" assert rows[0].reg_id == "12A20000" - assert session.calls[0]["url"] == "https://apis.data.go.kr/1360000/MidFcstInfoService/getMidSeaFcst" + assert ( + session.calls[0]["url"] + == "https://apis.data.go.kr/1360000/MidFcstInfoService/getMidSeaFcst" + ) -def test_mid_forecast_tm_fc_falls_back_to_request_value_when_row_omits_it() -> None: +async def test_mid_forecast_tm_fc_falls_back_to_request_value_when_row_omits_it() -> None: # 실서버 MidFcstInfoService 응답 row는 요청의 tmFc를 에코하지 않는다 (#20). # 응답에 tmFc가 없으면 요청에 실제로 사용한 tmFc로 폴백해야 한다. calls: list[tuple[Callable[[DataGoKrClient], Any], str, dict[str, Any]]] = [ @@ -637,7 +644,7 @@ def test_mid_forecast_tm_fc_falls_back_to_request_value_when_row_omits_it() -> N session = FakeSession(_payload(dict(row))) client = DataGoKrClient("decoded-key", session=session) - rows = call(client) + rows = await call(client) assert session.calls[0]["params"]["tmFc"] == "202606120600", operation assert rows[0].operation == operation @@ -646,7 +653,7 @@ def test_mid_forecast_tm_fc_falls_back_to_request_value_when_row_omits_it() -> N assert "tmFc" not in rows[0].raw, operation -def test_mid_forecast_tm_fc_prefers_response_row_value_over_request() -> None: +async def test_mid_forecast_tm_fc_prefers_response_row_value_over_request() -> None: session = FakeSession( _payload( { @@ -658,13 +665,13 @@ def test_mid_forecast_tm_fc_prefers_response_row_value_over_request() -> None: ) client = DataGoKrClient("decoded-key", session=session) - rows = client.mid_land_forecast(reg_id="11B00000", tm_fc="202606120600") + rows = await client.mid_land_forecast(reg_id="11B00000", tm_fc="202606120600") assert session.calls[0]["params"]["tmFc"] == "202606120600" assert rows[0].tm_fc == "202606111800" -def test_mid_forecast_tm_fc_fallback_handles_empty_row_value() -> None: +async def test_mid_forecast_tm_fc_fallback_handles_empty_row_value() -> None: session = FakeSession( _payload( { @@ -676,17 +683,17 @@ def test_mid_forecast_tm_fc_fallback_handles_empty_row_value() -> None: ) client = DataGoKrClient("decoded-key", session=session) - rows = client.mid_land_forecast(reg_id="11B00000", tm_fc="202606120600") + rows = await client.mid_land_forecast(reg_id="11B00000", tm_fc="202606120600") assert rows[0].tm_fc == "202606120600" -def test_mid_forecast_tm_fc_fallback_matches_auto_resolved_request_value() -> None: +async def test_mid_forecast_tm_fc_fallback_matches_auto_resolved_request_value() -> None: # when= 자동 해석(tm_fc 생략) 경로에서도 요청 param과 item 폴백이 같은 값을 본다. session = FakeSession(_payload({"regId": "11B00000", "wf3Am": "맑음"})) client = DataGoKrClient("decoded-key", session=session) - rows = client.mid_land_forecast( + rows = await client.mid_land_forecast( reg_id="11B00000", when=datetime(2026, 6, 12, 6, 5, tzinfo=KST), ) @@ -695,7 +702,7 @@ def test_mid_forecast_tm_fc_fallback_matches_auto_resolved_request_value() -> No assert rows[0].tm_fc == "202606111800" -def test_datagokr_asos_helpers_build_requests() -> None: +async def test_datagokr_asos_helpers_build_requests() -> None: daily_session = FakeSession( _payload( { @@ -728,12 +735,12 @@ def test_datagokr_asos_helpers_build_requests() -> None: ) ) - daily = DataGoKrClient("decoded-key", session=daily_session).asos_daily_weather( + daily = await DataGoKrClient("decoded-key", session=daily_session).asos_daily_weather( start_dt="20260501", end_dt="20260502", stn_ids=108, ) - hourly = DataGoKrClient("decoded-key", session=hourly_session).asos_hourly_weather( + hourly = await DataGoKrClient("decoded-key", session=hourly_session).asos_hourly_weather( start_dt="20260501", start_hh=3, end_dt="20260501", @@ -770,7 +777,7 @@ def test_datagokr_asos_helpers_build_requests() -> None: assert hourly[0].sea_level_pressure == 1013.1 -def test_datagokr_raw_weather_warning_and_message_helpers() -> None: +async def test_datagokr_raw_weather_warning_and_message_helpers() -> None: warning_session = FakeSession( _payload( { @@ -783,12 +790,12 @@ def test_datagokr_raw_weather_warning_and_message_helpers() -> None: ) message_session = FakeSession(_payload({"wfSv1": "summary"})) - warning = DataGoKrClient("decoded-key", session=warning_session).weather_warning_list( + warning = await DataGoKrClient("decoded-key", session=warning_session).weather_warning_list( stn_id=108, from_tm_fc="20260501", to_tm_fc="20260502", ) - land = DataGoKrClient("decoded-key", session=message_session).land_forecast_message( + land = await DataGoKrClient("decoded-key", session=message_session).land_forecast_message( reg_id="11B10101" ) @@ -810,7 +817,7 @@ def test_datagokr_raw_weather_warning_and_message_helpers() -> None: assert land[0].operation == "getLandFcst" -def test_datagokr_beach_forecast_helper_builds_request_and_models_rows() -> None: +async def test_datagokr_beach_forecast_helper_builds_request_and_models_rows() -> None: session = FakeSession( _payload( { @@ -828,7 +835,7 @@ def test_datagokr_beach_forecast_helper_builds_request_and_models_rows() -> None ) client = DataGoKrClient("decoded-key", session=session) - rows = client.beach_ultra_short_forecast( + rows = await client.beach_ultra_short_forecast( beach_num=1, base_date="20220622", base_time="1230", @@ -853,7 +860,7 @@ def test_datagokr_beach_forecast_helper_builds_request_and_models_rows() -> None assert "serviceKey" not in rows[0].metadata.request_params -def test_datagokr_beach_forecast_can_select_latest_base_from_when() -> None: +async def test_datagokr_beach_forecast_can_select_latest_base_from_when() -> None: session = FakeSession( _payload( { @@ -871,17 +878,19 @@ def test_datagokr_beach_forecast_can_select_latest_base_from_when() -> None: ) client = DataGoKrClient("decoded-key", session=session) - client.beach_forecast(beach_num="1", when=datetime(2026, 5, 7, 2, 5, tzinfo=KST)) + (await client.beach_forecast(beach_num="1", when=datetime(2026, 5, 7, 2, 5, tzinfo=KST))) assert session.calls[0]["params"]["base_date"] == "20260506" assert session.calls[0]["params"]["base_time"] == "2300" - assert_raises( - ValueError, - lambda: client.beach_forecast(beach_num="1", base_date="20260507"), + ( + await assert_raises( + ValueError, + lambda: client.beach_forecast(beach_num="1", base_date="20260507"), + ) ) -def test_datagokr_beach_observation_helpers_parse_rows() -> None: +async def test_datagokr_beach_observation_helpers_parse_rows() -> None: wave_client = DataGoKrClient( "decoded-key", session=FakeSession(_payload({"beachNum": "1", "tm": "202205011600", "wh": "0.7"})), @@ -891,8 +900,8 @@ def test_datagokr_beach_observation_helpers_parse_rows() -> None: session=FakeSession(_payload({"beachNum": "1", "tm": "202205011600", "tw": "18.4"})), ) - wave = wave_client.beach_wave_height(beach_num="1", search_time="202205011600") - water = water_client.beach_water_temperature( + wave = await wave_client.beach_wave_height(beach_num="1", search_time="202205011600") + water = await water_client.beach_water_temperature( beach_num=1, search_time=datetime(2022, 5, 1, 16, 0, tzinfo=KST), ) @@ -902,7 +911,7 @@ def test_datagokr_beach_observation_helpers_parse_rows() -> None: assert water[0].water_temperature == 18.4 -def test_datagokr_beach_tide_and_sun_helpers_preserve_upstream_parameters() -> None: +async def test_datagokr_beach_tide_and_sun_helpers_preserve_upstream_parameters() -> None: tide_client = DataGoKrClient( "decoded-key", session=FakeSession( @@ -930,8 +939,8 @@ def test_datagokr_beach_tide_and_sun_helpers_preserve_upstream_parameters() -> N ) sun_client = DataGoKrClient("decoded-key", session=sun_session) - tide = tide_client.beach_tide_info(beach_num=1, base_date="20220620") - sun = sun_client.beach_sun_info(beach_num="1", base_date="20220501") + tide = await tide_client.beach_tide_info(beach_num=1, base_date="20220620") + sun = await sun_client.beach_sun_info(beach_num="1", base_date="20220501") assert tide[0].station_name == "station" assert tide[0].tide_level == 35.0 @@ -941,21 +950,21 @@ def test_datagokr_beach_tide_and_sun_helpers_preserve_upstream_parameters() -> N assert sun[0].metadata.base_date == "20220501" -def test_datagokr_tour_living_and_earthquake_helpers() -> None: +async def test_datagokr_tour_living_and_earthquake_helpers() -> None: tour_session = FakeSession(_payload({"courseId": "1"})) living_session = FakeSession(_payload({"areaNo": "1100000000"})) quake_session = FakeSession(_payload({"tmFc": "20260501"})) - tour = DataGoKrClient("decoded-key", session=tour_session).tour_village_forecast( + tour = await DataGoKrClient("decoded-key", session=tour_session).tour_village_forecast( course_id=1, current_date="20260501", hour=9, ) - uv = DataGoKrClient("decoded-key", session=living_session).uv_index( + uv = await DataGoKrClient("decoded-key", session=living_session).uv_index( area_no="1100000000", time="2026050106", ) - quake = DataGoKrClient("decoded-key", session=quake_session).earthquake_message_list( + quake = await DataGoKrClient("decoded-key", session=quake_session).earthquake_message_list( from_tm_fc="20260501", to_tm_fc="20260502", ) @@ -977,7 +986,7 @@ def test_datagokr_tour_living_and_earthquake_helpers() -> None: assert quake[0].operation == "getEqkMsgList" -def test_datagokr_result_code_mapping_and_shape_errors() -> None: +async def test_datagokr_result_code_mapping_and_shape_errors() -> None: auth_client = DataGoKrClient( "bad-key", session=FakeSession( @@ -1002,15 +1011,15 @@ def test_datagokr_result_code_mapping_and_shape_errors() -> None: ) parse_client = DataGoKrClient("decoded-key", session=FakeSession({"bad": {}})) - assert_raises(KmaAuthError, lambda: auth_client.request("S", "O")) - assert_raises(KmaServerError, lambda: server_client.request("S", "O")) - assert_raises(KmaParseError, lambda: parse_client.request("S", "O")) + (await assert_raises(KmaAuthError, lambda: auth_client.request("S", "O"))) + (await assert_raises(KmaServerError, lambda: server_client.request("S", "O"))) + (await assert_raises(KmaParseError, lambda: parse_client.request("S", "O"))) -def test_datagokr_no_data_result_code_normalizes_to_empty_body() -> None: +async def test_datagokr_no_data_result_code_normalizes_to_empty_body() -> None: client = DataGoKrClient("decoded-key", session=FakeSession(_no_data_payload())) - body = client.request("MidFcstInfoService", "getMidFcst") + body = await client.request("MidFcstInfoService", "getMidFcst") assert body["items"] == {"item": []} assert body["totalCount"] == 0 @@ -1018,11 +1027,11 @@ def test_datagokr_no_data_result_code_normalizes_to_empty_body() -> None: assert has_next_page(body) is False -def test_datagokr_no_data_weather_warning_list_returns_empty_list() -> None: +async def test_datagokr_no_data_weather_warning_list_returns_empty_list() -> None: session = FakeSession(_no_data_payload()) client = DataGoKrClient("decoded-key", session=session) - warnings = client.weather_warning_list( + warnings = await client.weather_warning_list( stn_id=108, from_tm_fc="20260501", to_tm_fc="20260504", @@ -1034,37 +1043,37 @@ def test_datagokr_no_data_weather_warning_list_returns_empty_list() -> None: ) -def test_datagokr_no_data_mid_forecast_returns_empty_list() -> None: +async def test_datagokr_no_data_mid_forecast_returns_empty_list() -> None: client = DataGoKrClient("decoded-key", session=FakeSession(_no_data_payload())) - assert client.mid_forecast(stn_id=108, tm_fc="202605010600") == [] + assert (await client.mid_forecast(stn_id=108, tm_fc="202605010600")) == [] -def test_datagokr_no_data_iter_pages_stops_after_first_page() -> None: +async def test_datagokr_no_data_iter_pages_stops_after_first_page() -> None: session = FakeSession(_no_data_payload()) client = DataGoKrClient("decoded-key", session=session) - pages = list(client.iter_pages("WthrWrnInfoService", "getWthrWrnList")) + pages = [item async for item in client.iter_pages("WthrWrnInfoService", "getWthrWrnList")] assert len(pages) == 1 assert pages[0]["items"] == {"item": []} assert len(session.calls) == 1 -def test_datagokr_no_data_without_body_returns_empty_items() -> None: +async def test_datagokr_no_data_without_body_returns_empty_items() -> None: payload = {"response": {"header": {"resultCode": "03", "resultMsg": "NO_DATA"}}} client = DataGoKrClient("decoded-key", session=FakeSession(payload)) - assert client.items("WthrWrnInfoService", "getWthrWrnList") == [] + assert (await client.items("WthrWrnInfoService", "getWthrWrnList")) == [] -def test_datagokr_no_data_async_items_returns_empty_list() -> None: +async def test_datagokr_no_data_async_items_returns_empty_list() -> None: async def run() -> None: session = AsyncFakeSession(_no_data_payload()) - client = DataGoKrClient("decoded-key", async_session=session) + client = DataGoKrClient("decoded-key", session=session) - items = await client.aitems("WthrWrnInfoService", "getWthrWrnList") + items = await client.items("WthrWrnInfoService", "getWthrWrnList") assert items == [] - asyncio.run(run()) + await run() diff --git a/tests/test_http.py b/tests/test_http.py index 3266dc5..c4e86f5 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,7 +1,7 @@ from __future__ import annotations -import asyncio from typing import Any +from unittest.mock import AsyncMock import httpx import pytest @@ -27,7 +27,7 @@ def __init__(self, statuses: list[int]) -> None: self._statuses = list(statuses) self.calls = 0 - def get(self, url: str, *, params: Any, timeout: float) -> _Resp: + async def get(self, url: str, *, params: Any, timeout: float) -> _Resp: self.calls += 1 status = self._statuses.pop(0) return _Resp(status) @@ -58,12 +58,12 @@ def test_backoff_with_jitter_uses_random_uniform(monkeypatch: pytest.MonkeyPatch assert _http._backoff_with_jitter(0.3, 2) == pytest.approx(1.2) -def test_get_with_retries_retries_then_succeeds(monkeypatch: pytest.MonkeyPatch) -> None: +async def test_get_with_retries_retries_then_succeeds(monkeypatch: pytest.MonkeyPatch) -> None: slept: list[float] = [] - monkeypatch.setattr(_http.time, "sleep", lambda s: slept.append(s)) + monkeypatch.setattr(_http.asyncio, "sleep", AsyncMock(side_effect=slept.append)) client = _FakeClient([503, 200]) - response = _http.get_with_retries( + response = await _http.get_with_retries( client, "http://example.test", params=None, timeout=1, retries=3 ) @@ -72,31 +72,37 @@ def test_get_with_retries_retries_then_succeeds(monkeypatch: pytest.MonkeyPatch) assert len(slept) == 1 # one backoff between the two attempts -def test_get_with_retries_does_not_retry_on_404(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(_http.time, "sleep", lambda s: None) +async def test_get_with_retries_does_not_retry_on_404(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(_http.asyncio, "sleep", AsyncMock()) client = _FakeClient([404, 200]) with pytest.raises(httpx.HTTPStatusError): - _http.get_with_retries( - client, "http://example.test", params=None, timeout=1, retries=3 + ( + await _http.get_with_retries( + client, "http://example.test", params=None, timeout=1, retries=3 + ) ) assert client.calls == 1 # 404 is not retryable -def test_get_with_retries_exhausts_and_raises(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(_http.time, "sleep", lambda s: None) +async def test_get_with_retries_exhausts_and_raises(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(_http.asyncio, "sleep", AsyncMock()) client = _FakeClient([503, 503, 503]) with pytest.raises(httpx.HTTPStatusError): - _http.get_with_retries( - client, "http://example.test", params=None, timeout=1, retries=2 + ( + await _http.get_with_retries( + client, "http://example.test", params=None, timeout=1, retries=2 + ) ) assert client.calls == 3 # initial + 2 retries -def test_async_get_with_retries_retries_then_succeeds(monkeypatch: pytest.MonkeyPatch) -> None: +async def test_async_get_with_retries_retries_then_succeeds( + monkeypatch: pytest.MonkeyPatch, +) -> None: async def _no_sleep(_s: float) -> None: return None @@ -104,10 +110,10 @@ async def _no_sleep(_s: float) -> None: client = _AsyncFakeClient([502, 200]) async def run() -> None: - response = await _http.async_get_with_retries( + response = await _http.get_with_retries( client, "http://example.test", params=None, timeout=1, retries=3 ) assert response.status_code == 200 assert client.calls == 2 - asyncio.run(run()) + await run() diff --git a/tests/test_live_services.py b/tests/test_live_services.py index 66c684d..69bf66a 100644 --- a/tests/test_live_services.py +++ b/tests/test_live_services.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import os from collections.abc import Mapping from datetime import datetime, timedelta @@ -38,9 +37,9 @@ def _load_local_env() -> None: os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) -_load_local_env() - RUN_LIVE = os.getenv("KMA_RUN_LIVE") == "1" +if RUN_LIVE: + _load_local_env() def _apihub_key() -> str | None: @@ -72,61 +71,61 @@ def _assert_apihub_response_is_sanitized(response: ApiHubResponse) -> None: @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @pytest.mark.skipif(not _apihub_key(), reason="KMA_APIHUB_AUTH_KEY is not set") -def test_live_apihub_forecast_region_endpoints_shape() -> None: - client = ApiHubGeneratedClient(_apihub_key() or "", timeout=30, retries=1) +async def test_live_apihub_forecast_region_endpoints_shape() -> None: + async with ApiHubGeneratedClient(_apihub_key() or "", timeout=30, retries=1) as client: + responses = [ + (await client.fct_shrt_reg(use_sample=True)), + (await client.fct_medm_reg(use_sample=True)), + ] - responses = [ - client.fct_shrt_reg(use_sample=True), - client.fct_medm_reg(use_sample=True), - ] + for response in responses: + table = response.text_table() - for response in responses: - table = response.text_table() - - assert response.status_code == 200 - assert response.content - assert response.text.strip() - _assert_apihub_response_is_sanitized(response) - assert table.raw_lines - assert "SERVICE_KEY" not in response.text.upper() + assert response.status_code == 200 + assert response.content + assert response.text.strip() + _assert_apihub_response_is_sanitized(response) + assert table.raw_lines + assert "SERVICE_KEY" not in response.text.upper() @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @pytest.mark.skipif(not _apihub_key(), reason="KMA_APIHUB_AUTH_KEY is not set") -def test_live_apihub_warning_impact_and_zone_endpoints_shape() -> None: - generated = ApiHubGeneratedClient(_apihub_key() or "", timeout=30, retries=1) - generic = ApiHubClient(_apihub_key() or "", timeout=30, retries=1) - - text_responses = [ - generated.wrn_reg(use_sample=True), - generated.ifs_fct_pstt(use_sample=True), - ] - - for response in text_responses: - table = response.text_table() +async def test_live_apihub_warning_impact_and_zone_endpoints_shape() -> None: + async with ( + ApiHubGeneratedClient(_apihub_key() or "", timeout=30, retries=1) as generated, + ApiHubClient(_apihub_key() or "", timeout=30, retries=1) as generic, + ): + text_responses = [ + (await generated.wrn_reg(use_sample=True)), + (await generated.ifs_fct_pstt(use_sample=True)), + ] + + for response in text_responses: + table = response.text_table() + + assert response.status_code == 200 + assert response.content + assert response.text.strip() + _assert_apihub_response_is_sanitized(response) + assert table.raw_lines + assert "SERVICE_KEY" not in response.text.upper() + + zone_response = await generic.open_api( + "FcstZoneInfoService", + "getFcstZoneCd", + {"regId": "11A00101"}, + data_type="JSON", + num_of_rows=10, + ) + payload = zone_response.json() + body = payload["response"]["body"] - assert response.status_code == 200 - assert response.content - assert response.text.strip() - _assert_apihub_response_is_sanitized(response) - assert table.raw_lines - assert "SERVICE_KEY" not in response.text.upper() - - zone_response = generic.open_api( - "FcstZoneInfoService", - "getFcstZoneCd", - {"regId": "11A00101"}, - data_type="JSON", - num_of_rows=10, - ) - payload = zone_response.json() - body = payload["response"]["body"] - - assert zone_response.status_code == 200 - _assert_apihub_response_is_sanitized(zone_response) - assert payload["response"]["header"]["resultCode"] == "00" - assert body["items"]["item"] - assert int(body["totalCount"]) >= 1 + assert zone_response.status_code == 200 + _assert_apihub_response_is_sanitized(zone_response) + assert payload["response"]["header"]["resultCode"] == "00" + assert body["items"]["item"] + assert int(body["totalCount"]) >= 1 @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -134,27 +133,27 @@ def test_live_apihub_warning_impact_and_zone_endpoints_shape() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_data_gokr_ultra_srt_ncst_shape() -> None: - client = DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) - base_date, base_time = latest_ultra_srt_ncst_base() - - body = client.request( - "VilageFcstInfoService_2.0", - "getUltraSrtNcst", - { - "base_date": base_date, - "base_time": base_time, - "nx": 60, - "ny": 127, - }, - num_of_rows=100, - ) - items = _items_from_body(body) - - assert items - assert any(item.get("category") == "T1H" for item in items) - assert all(str(item.get("nx")) == "60" for item in items) - assert all(str(item.get("ny")) == "127" for item in items) +async def test_live_data_gokr_ultra_srt_ncst_shape() -> None: + async with DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) as client: + base_date, base_time = latest_ultra_srt_ncst_base() + + body = await client.request( + "VilageFcstInfoService_2.0", + "getUltraSrtNcst", + { + "base_date": base_date, + "base_time": base_time, + "nx": 60, + "ny": 127, + }, + num_of_rows=100, + ) + items = _items_from_body(body) + + assert items + assert any(item.get("category") == "T1H" for item in items) + assert all(str(item.get("nx")) == "60" for item in items) + assert all(str(item.get("ny")) == "127" for item in items) @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -162,9 +161,9 @@ def test_live_data_gokr_ultra_srt_ncst_shape() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_async_kma_forecast_facade_shape() -> None: +async def test_live_async_kma_forecast_facade_shape() -> None: async def run() -> None: - async with KmaClient.aio(_data_gokr_key() or "", timeout=30, retries=1) as client: + async with KmaClient(_data_gokr_key() or "", timeout=30, retries=1) as client: snapshot = await client.forecast.now(nx=60, ny=127) assert snapshot.metadata is not None @@ -173,7 +172,7 @@ async def run() -> None: assert snapshot.grid.ny == 127 assert snapshot.raw["items"] - asyncio.run(run()) + await run() @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -181,10 +180,10 @@ async def run() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_async_data_gokr_facade_shape() -> None: +async def test_live_async_data_gokr_facade_shape() -> None: async def run() -> None: base_date, base_time = latest_ultra_srt_ncst_base() - async with DataGoKrClient.aio(_data_gokr_key() or "", timeout=30, retries=1) as client: + async with DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) as client: items = await client.items( "VilageFcstInfoService_2.0", "getUltraSrtNcst", @@ -200,7 +199,7 @@ async def run() -> None: assert items assert any(item.get("category") == "T1H" for item in items) - asyncio.run(run()) + await run() @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -208,25 +207,24 @@ async def run() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_data_gokr_asos_daily_typed_model_shape() -> None: - client = DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) - - try: - rows = client.asos_daily_weather( - start_dt="20240101", - end_dt="20240102", - stn_ids=108, - num_of_rows=10, - ) - except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 - pytest.skip(f"AsosDalyInfoService not authorized for this service key: {exc}") +async def test_live_data_gokr_asos_daily_typed_model_shape() -> None: + async with DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) as client: + try: + rows = await client.asos_daily_weather( + start_dt="20240101", + end_dt="20240102", + stn_ids=108, + num_of_rows=10, + ) + except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 + pytest.skip(f"AsosDalyInfoService not authorized for this service key: {exc}") - assert rows - assert all(isinstance(row, AsosDailyItem) for row in rows) - assert rows[0].stn_id == "108" - assert rows[0].date - assert rows[0].metadata is not None - assert "serviceKey" not in rows[0].metadata.request_params + assert rows + assert all(isinstance(row, AsosDailyItem) for row in rows) + assert rows[0].stn_id == "108" + assert rows[0].date + assert rows[0].metadata is not None + assert "serviceKey" not in rows[0].metadata.request_params @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -234,26 +232,25 @@ def test_live_data_gokr_asos_daily_typed_model_shape() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_data_gokr_asos_hourly_typed_model_shape() -> None: - client = DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) - - try: - rows = client.asos_hourly_weather( - start_dt="20240101", - start_hh=0, - end_dt="20240101", - end_hh=3, - stn_ids=108, - num_of_rows=10, - ) - except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 - pytest.skip(f"AsosHourlyInfoService not authorized for this service key: {exc}") +async def test_live_data_gokr_asos_hourly_typed_model_shape() -> None: + async with DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) as client: + try: + rows = await client.asos_hourly_weather( + start_dt="20240101", + start_hh=0, + end_dt="20240101", + end_hh=3, + stn_ids=108, + num_of_rows=10, + ) + except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 + pytest.skip(f"AsosHourlyInfoService not authorized for this service key: {exc}") - assert rows - assert all(isinstance(row, AsosHourlyItem) for row in rows) - assert rows[0].stn_id == "108" - assert rows[0].observed_at - assert rows[0].metadata is not None + assert rows + assert all(isinstance(row, AsosHourlyItem) for row in rows) + assert rows[0].stn_id == "108" + assert rows[0].observed_at + assert rows[0].metadata is not None @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -261,36 +258,35 @@ def test_live_data_gokr_asos_hourly_typed_model_shape() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_data_gokr_weather_warning_typed_model_shape() -> None: - client = DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) - - # getWthrWrnList 는 현재 시각 기준 최근 6일 이내만 조회 가능하므로 실시간으로 범위를 잡는다. - now = datetime.now() - from_tm_fc = (now - timedelta(days=3)).strftime("%Y%m%d") - to_tm_fc = now.strftime("%Y%m%d") - - try: - # NO_DATA(resultCode 03)는 예외 대신 빈 list로 정규화된다 (#18). - rows = client.weather_warning_list( - stn_id=108, - from_tm_fc=from_tm_fc, - to_tm_fc=to_tm_fc, - num_of_rows=10, - ) - except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 - pytest.skip(f"WthrWrnInfoService not authorized for this service key: {exc}") +async def test_live_data_gokr_weather_warning_typed_model_shape() -> None: + async with DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) as client: + # getWthrWrnList 는 현재 시각 기준 최근 6일 이내만 조회 가능하므로 실시간으로 범위를 잡는다. + now = datetime.now() + from_tm_fc = (now - timedelta(days=3)).strftime("%Y%m%d") + to_tm_fc = now.strftime("%Y%m%d") + + try: + # NO_DATA(resultCode 03)는 예외 대신 빈 list로 정규화된다 (#18). + rows = await client.weather_warning_list( + stn_id=108, + from_tm_fc=from_tm_fc, + to_tm_fc=to_tm_fc, + num_of_rows=10, + ) + except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 + pytest.skip(f"WthrWrnInfoService not authorized for this service key: {exc}") - assert all(isinstance(row, WeatherWarningItem) for row in rows) - for row in rows: - assert row.metadata is not None - assert "serviceKey" not in row.metadata.request_params + assert all(isinstance(row, WeatherWarningItem) for row in rows) + for row in rows: + assert row.metadata is not None + assert "serviceKey" not in row.metadata.request_params @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @pytest.mark.skipif(not _apihub_key(), reason="KMA_APIHUB_AUTH_KEY is not set") -def test_live_async_apihub_facade_shape() -> None: +async def test_live_async_apihub_facade_shape() -> None: async def run() -> None: - async with ApiHubClient.aio(_apihub_key() or "", timeout=30, retries=1) as client: + async with ApiHubClient(_apihub_key() or "", timeout=30, retries=1) as client: response = await client.open_api( "FcstZoneInfoService", "getFcstZoneCd", @@ -304,7 +300,7 @@ async def run() -> None: _assert_apihub_response_is_sanitized(response) assert payload["response"]["header"]["resultCode"] == "00" - asyncio.run(run()) + await run() @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -312,25 +308,24 @@ async def run() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_data_gokr_mid_land_forecast_shape() -> None: - client = DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) - - try: - rows = client.mid_land_forecast(reg_id="11B00000", num_of_rows=10) - except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 - pytest.skip(f"MidFcstInfoService not authorized for this service key: {exc}") - - if not rows: # NO_DATA(03)는 빈 결과로 정규화된다 — 발표시각 데이터 아직 없음 (정상) - pytest.skip("MidFcstInfoService returned NO_DATA for the chosen tmFc") - - assert rows - assert all(isinstance(row, MidForecastItem) for row in rows) - assert rows[0].operation == "getMidLandFcst" - # 실서버 row는 tmFc를 에코하지 않는다 — 요청 tmFc 폴백이 채워져야 한다 (#20). - assert rows[0].tm_fc is not None - assert len(rows[0].tm_fc) == 12 and rows[0].tm_fc.isdigit() - assert rows[0].metadata is not None - assert "serviceKey" not in rows[0].metadata.request_params +async def test_live_data_gokr_mid_land_forecast_shape() -> None: + async with DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) as client: + try: + rows = await client.mid_land_forecast(reg_id="11B00000", num_of_rows=10) + except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 + pytest.skip(f"MidFcstInfoService not authorized for this service key: {exc}") + + if not rows: # NO_DATA(03)는 빈 결과로 정규화된다 — 발표시각 데이터 아직 없음 (정상) + pytest.skip("MidFcstInfoService returned NO_DATA for the chosen tmFc") + + assert rows + assert all(isinstance(row, MidForecastItem) for row in rows) + assert rows[0].operation == "getMidLandFcst" + # 실서버 row는 tmFc를 에코하지 않는다 — 요청 tmFc 폴백이 채워져야 한다 (#20). + assert rows[0].tm_fc is not None + assert len(rows[0].tm_fc) == 12 and rows[0].tm_fc.isdigit() + assert rows[0].metadata is not None + assert "serviceKey" not in rows[0].metadata.request_params @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -338,20 +333,19 @@ def test_live_data_gokr_mid_land_forecast_shape() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_data_gokr_land_forecast_message_shape() -> None: - client = DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) - - try: - rows = client.land_forecast_message(reg_id="11B10101", num_of_rows=10) - except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 - pytest.skip(f"VilageFcstMsgService not authorized for this service key: {exc}") +async def test_live_data_gokr_land_forecast_message_shape() -> None: + async with DataGoKrClient(_data_gokr_key() or "", timeout=30, retries=1) as client: + try: + rows = await client.land_forecast_message(reg_id="11B10101", num_of_rows=10) + except KmaAuthError as exc: # 서비스키 미구독 — docs/live-test-key-issues.md 참고 + pytest.skip(f"VilageFcstMsgService not authorized for this service key: {exc}") - if not rows: # NO_DATA(03)는 빈 결과로 정규화된다 - pytest.skip("VilageFcstMsgService returned NO_DATA") + if not rows: # NO_DATA(03)는 빈 결과로 정규화된다 + pytest.skip("VilageFcstMsgService returned NO_DATA") - assert rows - assert rows[0].operation == "getLandFcst" - assert rows[0].metadata is not None + assert rows + assert rows[0].operation == "getLandFcst" + assert rows[0].metadata is not None @pytest.mark.skipif(not RUN_LIVE, reason="set KMA_RUN_LIVE=1 to call real servers") @@ -359,13 +353,12 @@ def test_live_data_gokr_land_forecast_message_shape() -> None: not _data_gokr_key(), reason="DATA_GO_KR_SERVICE_KEY is not set", ) -def test_live_kma_vilage_forecast_short_shape() -> None: - client = KmaClient(_data_gokr_key() or "", timeout=30, retries=1) +async def test_live_kma_vilage_forecast_short_shape() -> None: + async with KmaClient(_data_gokr_key() or "", timeout=30, retries=1) as client: + items = await client.forecast_short(nx=60, ny=127) - items = client.forecast_short(nx=60, ny=127) - - assert items - assert all(isinstance(item, ForecastItem) for item in items) - assert all(item.nx == 60 and item.ny == 127 for item in items) - assert items[0].metadata is not None - assert items[0].forecast_at.tzinfo is not None # KST aware + assert items + assert all(isinstance(item, ForecastItem) for item in items) + assert all(item.nx == 60 and item.ny == 127 for item in items) + assert items[0].metadata is not None + assert items[0].forecast_at.tzinfo is not None # KST aware diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 980aacf..a2d0b34 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -5,6 +5,7 @@ import pytest from kma import has_next_page, iter_pages, next_page_no +from kma.pagination import PaginationLimitWarning def _body( @@ -44,7 +45,7 @@ def test_next_page_no_increments_then_stops() -> None: assert next_page_no(_body(page_no=3, num_of_rows=10, total_count=25)) is None -def test_iter_pages_follows_metadata_until_last_page() -> None: +async def test_iter_pages_follows_metadata_until_last_page() -> None: pages = { 1: _body(page_no=1, num_of_rows=2, total_count=5, item_count=2), 2: _body(page_no=2, num_of_rows=2, total_count=5, item_count=2), @@ -52,55 +53,56 @@ def test_iter_pages_follows_metadata_until_last_page() -> None: } fetched: list[int] = [] - def fetch(page_no: int) -> dict[str, Any]: + async def fetch(page_no: int) -> dict[str, Any]: fetched.append(page_no) return pages[page_no] - collected = list(iter_pages(fetch)) + collected = [item async for item in iter_pages(fetch)] assert fetched == [1, 2, 3] assert len(collected) == 3 -def test_iter_pages_respects_max_pages_safety_valve() -> None: +async def test_iter_pages_respects_max_pages_safety_valve() -> None: # totalCount never satisfied -> would loop forever without max_pages. - def fetch(page_no: int) -> dict[str, Any]: + async def fetch(page_no: int) -> dict[str, Any]: return _body(page_no=page_no, num_of_rows=1, total_count=10_000, item_count=1) - collected = list(iter_pages(fetch, max_pages=4)) + with pytest.warns(PaginationLimitWarning): + collected = [item async for item in iter_pages(fetch, max_pages=4)] assert len(collected) == 4 -def test_iter_pages_respects_max_items_safety_valve() -> None: - def fetch(page_no: int) -> dict[str, Any]: +async def test_iter_pages_respects_max_items_safety_valve() -> None: + async def fetch(page_no: int) -> dict[str, Any]: return _body(page_no=page_no, num_of_rows=3, total_count=10_000, item_count=3) - collected = list(iter_pages(fetch, max_items=5)) + collected = [item async for item in iter_pages(fetch, max_items=5)] # stops once items_seen (3 + 3 = 6) >= 5 assert len(collected) == 2 -def test_iter_pages_validates_arguments() -> None: - def fetch(_page_no: int) -> dict[str, Any]: +async def test_iter_pages_validates_arguments() -> None: + async def fetch(_page_no: int) -> dict[str, Any]: return _body(page_no=1, num_of_rows=1, total_count=1) with pytest.raises(ValueError): - list(iter_pages(fetch, start_page=0)) + [item async for item in iter_pages(fetch, start_page=0)] with pytest.raises(ValueError): - list(iter_pages(fetch, max_pages=0)) + [item async for item in iter_pages(fetch, max_pages=0)] with pytest.raises(ValueError): - list(iter_pages(fetch, max_items=0)) + [item async for item in iter_pages(fetch, max_items=0)] -def test_iter_pages_honors_start_page() -> None: +async def test_iter_pages_honors_start_page() -> None: seen: list[int] = [] - def fetch(page_no: int) -> dict[str, Any]: + async def fetch(page_no: int) -> dict[str, Any]: seen.append(page_no) return _body(page_no=page_no, num_of_rows=10, total_count=10, item_count=1) - list(iter_pages(fetch, start_page=5)) + [item async for item in iter_pages(fetch, start_page=5)] assert seen == [5] diff --git a/tests/test_ratelimit.py b/tests/test_ratelimit.py new file mode 100644 index 0000000..ae675c0 --- /dev/null +++ b/tests/test_ratelimit.py @@ -0,0 +1,165 @@ +"""공통 토큰 버킷의 동시성·취소·TPS 계약 검증.""" + +from __future__ import annotations + +import asyncio +import time +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +import kma._ratelimit as ratelimit + +AsyncTokenBucket = ratelimit.AsyncTokenBucket + + +class TokenBucketValidationTests(unittest.TestCase): + def test_invalid_rate_and_capacity(self) -> None: + for rate in (0, -1, float("nan"), float("inf"), -float("inf"), True): + with self.subTest(rate=rate), self.assertRaises(ValueError): + AsyncTokenBucket(rate) + for capacity in (0, -1, 0.5, float("nan"), float("inf"), True): + with self.subTest(capacity=capacity), self.assertRaises(ValueError): + AsyncTokenBucket(5, capacity=capacity) + + def test_capacity_defaults_and_read_only_configuration(self) -> None: + self.assertEqual(AsyncTokenBucket(0.5).capacity, 1) + self.assertEqual(AsyncTokenBucket(5).capacity, 5) + bucket = AsyncTokenBucket(5, capacity=1) + self.assertEqual(bucket.capacity, 1) + for name in ("max_rps", "capacity"): + with self.subTest(name=name), self.assertRaises(AttributeError): + setattr(bucket, name, 0) + + def test_cross_loop_use_fails_explicitly(self) -> None: + bucket = AsyncTokenBucket() + asyncio.run(bucket.acquire()) + with self.assertRaisesRegex(RuntimeError, "single event loop"): + asyncio.run(bucket.acquire()) + + +class TokenBucketAsyncTests(unittest.IsolatedAsyncioTestCase): + async def test_cancelled_wait_does_not_spend_the_following_token(self) -> None: + now = 100.0 + delays: list[float] = [] + entered = asyncio.Event() + release = asyncio.Event() + + async def sleep(delay: float) -> None: + nonlocal now + delays.append(delay) + entered.set() + await release.wait() + now += delay + + fake_asyncio = SimpleNamespace( + Lock=asyncio.Lock, get_running_loop=asyncio.get_running_loop, sleep=sleep + ) + with ( + patch.object(ratelimit, "time", SimpleNamespace(monotonic=lambda: now)), + patch.object(ratelimit, "asyncio", fake_asyncio), + ): + bucket = AsyncTokenBucket(0.5) + await bucket.acquire() + head = asyncio.create_task(bucket.acquire()) + await entered.wait() + head.cancel() + with self.assertRaises(asyncio.CancelledError): + await head + entered.clear() + follower = asyncio.create_task(bucket.acquire()) + await entered.wait() + release.set() + await follower + self.assertEqual(delays, [2.0, 2.0]) + self.assertEqual(now, 102.0) + + async def test_fractional_tps_refills_and_idle_burst_is_capped(self) -> None: + now = 100.0 + waits: list[float] = [] + + async def sleep(delay: float) -> None: + nonlocal now + waits.append(delay) + now += delay + await asyncio.sleep(0) + + fake_asyncio = SimpleNamespace( + Lock=asyncio.Lock, get_running_loop=asyncio.get_running_loop, sleep=sleep + ) + with ( + patch.object(ratelimit, "time", SimpleNamespace(monotonic=lambda: now)), + patch.object(ratelimit, "asyncio", fake_asyncio), + ): + bucket = AsyncTokenBucket(0.5) + await bucket.acquire() + self.assertEqual(waits, []) + await bucket.acquire() + self.assertEqual(waits, [2.0]) + now += 100 + await bucket.acquire() + await bucket.acquire() + self.assertEqual(waits, [2.0, 2.0]) + + async def test_concurrent_callers_observe_fifo_and_total_budget(self) -> None: + bucket = AsyncTokenBucket(100, capacity=1) + await bucket.acquire() + started = time.monotonic() + grants: list[tuple[int, float]] = [] + + async def request(index: int) -> None: + await bucket.acquire() + grants.append((index, time.monotonic() - started)) + + await asyncio.wait_for(asyncio.gather(*(request(i) for i in range(10))), 3) + self.assertEqual([i for i, _ in grants], list(range(10))) + for index, elapsed in grants: + self.assertGreaterEqual(elapsed + 0.003, (index + 1) / 100) + + async def test_cancelled_head_and_queued_waiter_do_not_stall_followers(self) -> None: + now = 0.0 + sleeping = asyncio.Event() + release = asyncio.Event() + + async def sleep(delay: float) -> None: + nonlocal now + sleeping.set() + await release.wait() + now += delay + + fake_asyncio = SimpleNamespace( + Lock=asyncio.Lock, get_running_loop=asyncio.get_running_loop, sleep=sleep + ) + with ( + patch.object(ratelimit, "time", SimpleNamespace(monotonic=lambda: now)), + patch.object(ratelimit, "asyncio", fake_asyncio), + ): + bucket = AsyncTokenBucket(100, capacity=1) + await bucket.acquire() + head = asyncio.create_task(bucket.acquire()) + await sleeping.wait() + queued = asyncio.create_task(bucket.acquire()) + follower = asyncio.create_task(bucket.acquire()) + await asyncio.sleep(0) + head.cancel() + queued.cancel() + outcomes = await asyncio.gather(head, queued, return_exceptions=True) + self.assertTrue(all(isinstance(value, asyncio.CancelledError) for value in outcomes)) + release.set() + await asyncio.wait_for(follower, 1) + await asyncio.wait_for(bucket.acquire(), 1) + + async def test_cancelling_only_waiter_leaves_no_background_task(self) -> None: + bucket = AsyncTokenBucket(0.1, capacity=1) + await bucket.acquire() + waiting = asyncio.create_task(bucket.acquire()) + await asyncio.sleep(0) + waiting.cancel() + with self.assertRaises(asyncio.CancelledError): + await waiting + self.assertFalse(bucket._lock.locked()) + self.assertEqual(asyncio.all_tasks(), {asyncio.current_task()}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/update_apihub_endpoints.py b/tools/update_apihub_endpoints.py index 65b75be..eaef85f 100644 --- a/tools/update_apihub_endpoints.py +++ b/tools/update_apihub_endpoints.py @@ -9,6 +9,7 @@ from __future__ import annotations +import asyncio import html import json import keyword @@ -23,6 +24,9 @@ import httpx +from kma import AsyncTokenBucket +from kma._http import get_with_retries + BASE_URL = "https://apihub.kma.go.kr" ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "src" / "kma" / "apihub_endpoints.py" @@ -74,10 +78,11 @@ class Attachment: kind: str -def main() -> None: - with httpx.Client(follow_redirects=True) as session: - endpoints = scrape_endpoints(session) - attachments = scrape_attachments(session) +async def main() -> None: + async with httpx.AsyncClient(follow_redirects=True) as session: + budget = AsyncTokenBucket(5) + endpoints = await scrape_endpoints(session, budget) + attachments = await scrape_attachments(session, budget) assign_names(endpoints) OUTPUT.write_text(render_module(endpoints, attachments), encoding="utf-8") DOC_OUTPUT.write_text(render_docs(endpoints, attachments), encoding="utf-8") @@ -86,18 +91,22 @@ def main() -> None: print(f"wrote endpoint catalog to {DOC_OUTPUT}") -def scrape_endpoints(session: httpx.Client) -> list[Endpoint]: +async def scrape_endpoints( + session: httpx.AsyncClient, budget: AsyncTokenBucket | None = None +) -> list[Endpoint]: + budget = budget if budget is not None else AsyncTokenBucket(5) merged: OrderedDict[tuple[str, tuple[tuple[str, str], ...]], Endpoint] = OrderedDict() services: list[tuple[int, int, str]] = [] for category_id in CATEGORY_IDS: - page = get_text(session, "/apiList.do", {"seqApi": category_id}) + page = await get_text(session, budget, "/apiList.do", {"seqApi": category_id}) for service in parse_const_array(page, "apiList"): service_id = int(service["seqApi"]) service_name = str(service["nmApi"]) services.append((category_id, service_id, service_name)) - service_page = get_text( + service_page = await get_text( session, + budget, "/apiList.do", {"seqApi": category_id, "seqApiSub": service_id}, ) @@ -105,8 +114,9 @@ def scrape_endpoints(session: httpx.Client) -> list[Endpoint]: merge_endpoint(merged, endpoint) try: - generator_page = get_text( + generator_page = await get_text( session, + budget, "/generateAPIUrl.do", {"seqApi": category_id, "seqApiSub": service_id}, ) @@ -124,8 +134,9 @@ def scrape_endpoints(session: httpx.Client) -> list[Endpoint]: ): merge_endpoint(merged, endpoint) - for endpoint in parse_text_attachment_examples( + for endpoint in await parse_text_attachment_examples( session, + budget, category_id, service_id, service_name, @@ -136,16 +147,20 @@ def scrape_endpoints(session: httpx.Client) -> list[Endpoint]: return list(merged.values()) -def scrape_attachments(session: httpx.Client) -> list[Attachment]: +async def scrape_attachments( + session: httpx.AsyncClient, budget: AsyncTokenBucket | None = None +) -> list[Attachment]: + budget = budget if budget is not None else AsyncTokenBucket(5) attachments: list[Attachment] = [] seen: set[tuple[int, int, str, str]] = set() for category_id in CATEGORY_IDS: - page = get_text(session, "/apiList.do", {"seqApi": category_id}) + page = await get_text(session, budget, "/apiList.do", {"seqApi": category_id}) for service in parse_const_array(page, "apiList"): service_id = int(service["seqApi"]) service_name = str(service["nmApi"]) - service_page = get_text( + service_page = await get_text( session, + budget, "/apiList.do", {"seqApi": category_id, "seqApiSub": service_id}, ) @@ -163,8 +178,12 @@ def scrape_attachments(session: httpx.Client) -> list[Attachment]: return attachments -def get_text(session: httpx.Client, path: str, params: dict[str, Any]) -> str: - response = session.get(f"{BASE_URL}{path}", params=params, timeout=30) +async def get_text( + session: httpx.AsyncClient, budget: AsyncTokenBucket, path: str, params: dict[str, Any] +) -> str: + response = await get_with_retries( + session, f"{BASE_URL}{path}", params=params, timeout=30, retries=0, rate_limiter=budget + ) response.raise_for_status() return response.text @@ -241,8 +260,9 @@ def parse_generator_page( return endpoints -def parse_text_attachment_examples( - session: httpx.Client, +async def parse_text_attachment_examples( + session: httpx.AsyncClient, + budget: AsyncTokenBucket, category_id: int, service_id: int, service_name: str, @@ -261,7 +281,14 @@ def parse_text_attachment_examples( if "예제" not in label and filename.lower() != "main.txt": continue try: - response = session.get(f"{BASE_URL}{href}", timeout=30) + response = await get_with_retries( + session, + f"{BASE_URL}{href}", + params=None, + timeout=30, + retries=0, + rate_limiter=budget, + ) response.raise_for_status() except httpx.HTTPError: continue @@ -517,7 +544,7 @@ def render_module(endpoints: list[Endpoint], attachments: list[Attachment]) -> s " def sample_params(self, name: str) -> Mapping[str, str]:", " return self.endpoint(name).sample_params", "", - " def call_endpoint(", + " async def call_endpoint(", " self,", " name: str,", " params: Mapping[str, Any] | None = None,", @@ -530,11 +557,11 @@ def render_module(endpoints: list[Endpoint], attachments: list[Attachment]) -> s " request_params.update(spec.sample_params)", " if params:", " request_params.update(params)", - " if any(kind == \"bare\" for kind, _name in spec.query_parts):", - " return self.request_query_parts(spec.path, spec.query_parts, request_params)", - " return self.request_path(spec.path, request_params)", + ' if any(kind == "bare" for kind, _name in spec.query_parts):', + " return await self.request_query_parts(spec.path, spec.query_parts, request_params)", + " return await self.request_path(spec.path, request_params)", "", - " def text_endpoint(", + " async def text_endpoint(", " self,", " name: str,", " params: Mapping[str, Any] | None = None,", @@ -542,18 +569,18 @@ def render_module(endpoints: list[Endpoint], attachments: list[Attachment]) -> s " use_sample: bool = False,", " delimiter: str | None = None,", " ) -> ApiHubTextTable:", - " return self.call_endpoint(name, params, use_sample=use_sample).text_table(", + " return (await self.call_endpoint(name, params, use_sample=use_sample)).text_table(", " delimiter=delimiter", " )", "", - " def image_endpoint(", + " async def image_endpoint(", " self,", " name: str,", " params: Mapping[str, Any] | None = None,", " *,", " use_sample: bool = False,", " ) -> ApiHubImage:", - " return self.call_endpoint(name, params, use_sample=use_sample).image()", + " return (await self.call_endpoint(name, params, use_sample=use_sample)).image()", "", ] ) @@ -602,28 +629,52 @@ def render_docs(endpoints: list[Endpoint], attachments: list[Attachment]) -> str "", "## 사용법", "", - "```python", - "from kma import ApiHubGeneratedClient", - "", - "hub = ApiHubGeneratedClient.from_env()", - "response = hub.kma_sfctm2(tm=\"202605010900\", stn=\"108\", help=\"1\")", - "rows = response.text_table().rows", - "```", + '```python', + 'import asyncio', + 'from kma import ApiHubGeneratedClient', + '', + '', + 'async def main() -> None:', + ' async with ApiHubGeneratedClient.from_env() as hub:', + ' response = (await hub.kma_sfctm2(tm="202605010900", stn="108", help="1"))', + ' rows = response.text_table().rows', + '', + '', + 'asyncio.run(main())', + '```', "", "홈페이지 예제 값을 그대로 써서 호출하려면 `use_sample=True`를 넘깁니다. 실제 운영 코드에서는 예제 날짜가 오래되었을 수 있으므로 필요한 인자를 명시하는 것을 권장합니다.", "", - "```python", - "response = hub.kma_sfctm2(use_sample=True, stn=\"108\")", - "```", + '```python', + 'from kma import ApiHubGeneratedClient', + 'import asyncio', + '', + '', + 'async def main() -> None:', + ' async with ApiHubGeneratedClient.from_env() as hub:', + ' response = (await hub.kma_sfctm2(use_sample=True, stn="108"))', + '', + '', + 'asyncio.run(main())', + '```', "", "이미지 endpoint는 bytes와 포맷/크기 정보를 함께 얻을 수 있습니다.", "", - "```python", - "image = hub.image_endpoint(\"api_iwa_img_url_api_ret_grid_img\", use_sample=True)", - "print(image.format, image.width, image.height)", - "```", + '```python', + 'from kma import ApiHubGeneratedClient', + 'import asyncio', + '', + '', + 'async def main() -> None:', + ' async with ApiHubGeneratedClient.from_env() as hub:', + ' image = (await hub.image_endpoint("api_iwa_img_url_api_ret_grid_img", use_sample=True))', + ' print(image.format, image.width, image.height)', + '', + '', + 'asyncio.run(main())', + '```', "", - "이름 없는 query string을 쓰는 legacy 그래픽 URL은 `arg1`, `arg2`처럼 순서형 인자로 노출합니다. 예를 들어 `?202305031000&0&...` 형태는 `arg1=\"202305031000\"`, `arg2=\"0\"`로 넘깁니다.", + '이름 없는 query string을 쓰는 legacy 그래픽 URL은 `arg1`, `arg2`처럼 순서형 인자로 노출합니다. 예를 들어 `?202305031000&0&...` 형태는 `arg1="202305031000"`, `arg2="0"`로 넘깁니다.', "", "## 첨부 자료 metadata", "", @@ -659,7 +710,9 @@ def render_docs(endpoints: list[Endpoint], attachments: list[Attachment]) -> str lines.append("") for category_id in CATEGORY_IDS: - category_endpoints = [endpoint for endpoint in endpoints if endpoint.category_id == category_id] + category_endpoints = [ + endpoint for endpoint in endpoints if endpoint.category_id == category_id + ] if not category_endpoints: continue lines.extend( @@ -723,18 +776,18 @@ def render_method(endpoint: Endpoint) -> list[str]: doc = f"{endpoint.title}\n\nPath: {endpoint.path}\n파라미터: {params}" doc = "\n".join(textwrap.wrap(doc, width=88, replace_whitespace=False)) lines = [ - f" def {endpoint.name}(", + f" async def {endpoint.name}(", " self,", " *,", " use_sample: bool = False,", " **params: Any,", " ) -> ApiHubResponse:", f' """{doc}"""', - f" return self.call_endpoint({endpoint.name!r}, params, use_sample=use_sample)", + f" return await self.call_endpoint({endpoint.name!r}, params, use_sample=use_sample)", "", ] return lines if __name__ == "__main__": - main() + asyncio.run(main())