Skip to content

Latest commit

ย 

History

History
347 lines (258 loc) ยท 22.5 KB

implementation-status.md

File metadata and controls

347 lines (258 loc) ยท 22.5 KB

python-opinet-api Implementation Status

์‹œ๊ฐ ์ž์› ์ฐจ๋‹จ์€ Chromium CDP Fetch์˜ ImageยทFontยทMedia ํƒ€์ž…์—๋งŒ ์ ์šฉํ•œ๋‹ค. HTTP ์บ์‹œ๋ฅผ ๋„๋Š” Playwright ์š”์ฒญ ๋ผ์šฐํŒ…์„ ์ œ๊ฑฐํ–ˆ๋‹ค. ๋กœ์ปฌ HTTP ์„œ๋ฒ„์™€ ์‹ค์ œ Chromium์—์„œ ์ดˆ๊ธฐ GETยท ๋ฐ˜๋ณต POST์˜ JSยทCSS ์บ์‹œ ์žฌ์‚ฌ์šฉ๊ณผ ๊ฐ€๊ฒฉ fetchยท์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ยท์Šคํƒ€์ผ ์ ์šฉ์„ ๊ฒ€์ฆํ•œ๋‹ค.

์ฒดํฌํฌ์ธํŠธ ์ž‘์„ฑ์ž ๊ฒฝํ•ฉ์€ OpinetCheckpointBusyError๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค. ํ˜ธ์ถœ์ž๋Š” ์ง„ํ–‰ ์ค‘์ธ ๋‹ค๋ฅธ ์ˆ˜์ง‘๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ƒํƒœ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. SQLite I/OยทํŒŒ์ผ ์˜ค๋ฅ˜๋Š” ๊ฒฝํ•ฉ์œผ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค.

2026-10-24 ์ถ”๊ฐ€: ๊ธฐ๋ณธ ๋ฌด์ž‘์œ„ ์กฐ์ž‘ ๋Œ€๊ธฐ๋ฅผ 0.15~0.85์ดˆ๋กœ ๋‹จ์ถ•ํ•˜๊ณ  ์‹œ๊ตฐ๊ตฌ ์„ ํƒ์˜ ์ •์ƒ ์ž๋™ ์กฐํšŒ ์‘๋‹ต์„ ๊ฒ€์ฆํ•ด ์ค‘๋ณต ๋ฒ„ํŠผ ์กฐํšŒ๋ฅผ ์ค„์ธ๋‹ค. ์‹œ๋„ ์‘๋‹ต ๋ชฉ๋ก๊ณผ ์‹ค์ œ ์˜ต์…˜์„ ๋Œ€์กฐํ•˜๋ฉฐ ๊ฐ€๊ฒฉ ํŒŒ์‹ฑ์— ์“ฐ์ง€ ์•Š๋Š” ์‹œ๊ฐ ์ž์›์„ ์ œ์™ธํ•œ๋‹ค. ๋ฌธ์„œ ์™„๋ฃŒยท์ง€์—ญยทํƒญ ๊ฒ€์ฆ์„ ์œ ์ง€ํ•ด ๋™์  ์ถ”๊ฐ€ ํ–‰์„ ๋น ๋œจ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. ์šด์˜ ์ „๊ตญ30๋ถ„ ๋ชฉํ‘œ๋Š” ์‹ค์ œ ์‹คํ–‰์œผ๋กœ ๋ณ„๋„ ํ™•์ธํ•œ๋‹ค.

2026-10-24: ์ง€์—ญ๋ณ„ ์˜์† ์ฒดํฌํฌ์ธํŠธยท์™„๋ฃŒ ๊ฒฐ๊ณผ ์žฌ์ „๋‹ฌยท๋‹จ์ผ ์ž‘์„ฑ์ž ์ž ๊ธˆยท์‹œ๊ตฐ๊ตฌ ํƒ์ƒ‰ ์ตœ์ ํ™”๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ๋ณต๊ตฌ ๊ณ„์•ฝ์„ ๋”ฐ๋ฅธ๋‹ค. ์‹ค์ œ ์šด์˜ ์†๋„ ๊ฐœ์„ ๋ฅ ์€ ๋ฏธ์ธก์ •์ด๋ฉฐ ๋‹จ๊ณ„๋ณ„ ๋ˆ„์  ์‹œ๊ฐ„ ๋กœ๊ทธ๋กœ ์ง€์—ญ ์„ ํƒยท์‘๋‹ตยท๊ฒฐ๊ณผ ์ค€๋น„ ๋น„์šฉ์„ ๊ตฌ๋ถ„ํ•œ๋‹ค.

์ด ๋ฌธ์„œ๋Š” ํ˜„์žฌ ๊ตฌํ˜„ ์ƒํƒœ, ๊ฒ€์ฆ ๋ฐฉ๋ฒ•, ๋‹ค์Œ ์ž‘์—…์ž๊ฐ€ ๋ฐ˜๋ณตํ•˜์ง€ ๋ง์•„์•ผ ํ•  ํŒ๋‹จ์„ ํ•œ๊ณณ์— ๋ชจ์๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋ฒ”์œ„

OpinetClient๋Š” ์˜คํ”ผ๋„ท ๊ณต์‹ ์˜คํ”ˆ API ํŽ˜์ด์ง€์— ๋“ฑ์žฌ๋œ 5๊ฐœ ์—”๋“œํฌ์ธํŠธ๋งŒ ์•ˆ์ • ๊ตฌํ˜„ ๋Œ€์ƒ์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค.

2026-09-19 ๊ณต๊ฐœ ์ง€์—ญ๋ณ„ ํ™”๋ฉด ์ˆ˜์ง‘๊ธฐ

์šด์˜ Chromium์—์„œ ๊ฒ€์ƒ‰ ํ™”๋ฉด์˜ ์ž๋™ ์ด๋™ ์งํ›„ ์ด์ „ ์‘๋‹ต ๋ณธ๋ฌธ์ด ์—†์–ด์ง€๋Š” ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ–ˆ๋‹ค. ๊ฐ™์€ ์ถœ์ฒ˜ยท๊ฒ€์ƒ‰ ๋ฌธ์„œ์˜ ํ˜„์žฌ HTML๋งŒ ๋Œ€์ฒด ๊ฒ€์‚ฌํ•˜๋„๋ก ๋ณด์™„ํ–ˆ์œผ๋ฉฐ, ์ฐจ๋‹จ ํ™”๋ฉด๊ณผ ์™ธ๋ถ€ ์ด๋™, ๊ด€๋ จ ์—†๋Š” ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋Š” ๊ณ„์† ์‹คํŒจ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์šด์˜ ์ˆ˜์ง‘์—์„œ ์ผ์‹œ์ ์œผ๋กœ ์ง€์—ฐ๋œ ํ™”๋ฉด ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ๋„๋ก collector์˜ ๊ธฐ๋ณธ timeout_ms๋Š” 60์ดˆ๋กœ ๋‘”๋‹ค. ์ด ๋ณ€๊ฒฝ์€ ์š”์ฒญ ์žฌ์‹œ๋„๋‚˜ ์ ‘๊ทผ ์ œํ•œ ์šฐํšŒ๋ฅผ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๋Š”๋‹ค. 2026-10-12๋ถ€ํ„ฐ ์ง€์—ญ ์กฐํšŒ ํ•œ ๊ฑด์˜ '์กฐํšŒ' ๊ฒ€์ƒ‰ ์‘๋‹ต์ด ์‹œ๊ฐ„ ์ดˆ๊ณผ๋˜๋ฉด ๊ฒ€์ƒ‰ ํ™”๋ฉด์„ ์ƒˆ๋กœ ์—ด๊ณ  ํƒญยท์ง€์—ญ์„ ๋‹ค์‹œ ๊ณ ๋ฅธ ๋’ค ๊ทธ ์ง€์—ญ๋งŒ ํ•œ ๋ฒˆ ๋‹ค์‹œ ์กฐํšŒํ•œ๋‹ค(์ง€์—ญ ์„ ํƒยท๊ฒฐ๊ณผ DOM ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ๋Š” ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š๋Š”๋‹ค). ์žฌ์‹œ๋„๋Š” ํ™”๋ฉด ์กฐ์ž‘ ๊ฐ„๊ฒฉ์„ ์ง€ํ‚ค๊ณ  max_search_requests ์ƒํ•œ์—์„œ ์ฐจ๊ฐ๋˜๋ฉฐ, ์‹คํŒจ ์ง„๋‹จ์—๋Š” target=non-numeric ๋Œ€์‹  ์ •์ œยท์ ˆ๋‹จํ•œ ์ง€์—ญ ์‹๋ณ„์ž์™€ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ๋‚จ๋Š”๋‹ค.

๊ณต์‹ open API์™€ ๋ถ„๋ฆฌ๋œ opinet.experimental.OpinetBrowserCollector๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. Playwright๋กœ ์ง€์—ญ๋ณ„ ๊ณต๊ฐœ ํ™”๋ฉด์˜ ์‹œ๋„ยท์‹œ๊ตฐ๊ตฌยท์๋ฉด๋™ ์„ ํƒ๊ฐ’์„ ์ฝ๊ณ , ํ™”๋ฉด ์กฐํšŒ ์‹œ ๋ฐœ์ƒํ•˜๋Š” searRgSelect.do HTML ๋˜๋Š” searRgCircleAjax.do JSON ์‘๋‹ต์—์„œ ์ฃผ์œ ์†Œ์™€ ์ถฉ์ „์†Œ ๋ฐ์ดํ„ฐ๋ฅผ typed dataclass๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์ฃผ์œ ์†Œ ์กฐํšŒ ๋‹จ์œ„๋Š” ์‹œ๊ตฐ๊ตฌ์ด๋ฉฐ, ํ•„์š”ํ•  ๋•Œ๋งŒ query_level="dong"์œผ๋กœ ์๋ฉด๋™๋ณ„ ์กฐํšŒ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

ํ™”๋ฉด ์กฐ์ž‘ ์‚ฌ์ด์˜ ์งง์€ ๋ฌด์ž‘์œ„ ์ง€์—ฐ๊ณผ ์ „์ฒด ์ˆ˜์ง‘ ์‚ฌ์ด์˜ ๊ธฐ๋ณธ 8์‹œ๊ฐ„ ๊ฐ„๊ฒฉ, ์ตœ๊ทผ 24์‹œ๊ฐ„ ์ตœ๋Œ€ 3ํšŒ ์‹คํ–‰ ์ œํ•œ์€ ์š”์ฒญ ๋ถ€ํ•˜๋ฅผ ๋ถ„์‚ฐํ•˜๊ธฐ ์œ„ํ•œ ์ •์ฑ…์ž…๋‹ˆ๋‹ค. ์ง€์—ญ ์„ ํƒ AJAX์™€ ๊ฒ€์ƒ‰ ์‘๋‹ต ์™„๋ฃŒ, HTTP ์ƒํƒœยท์ ‘๊ทผ ๊ฑฐ๋ถ€ ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•˜๋ฉฐ, ์ž๋™ํ™” ํƒ์ง€ ์šฐํšŒยท์ง€๋ฌธ ์กฐ์ž‘ยทCAPTCHA ์šฐํšŒ๋Š” ๊ตฌํ˜„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ƒ์„ธ ์‚ฌ์šฉ๋ฒ•๊ณผ ์ด์šฉ์•ฝ๊ด€ยท์žฌ๋ฐฐํฌ ํ™•์ธ ์กฐ๊ฑด์€ docs/browser-collection.md์— ๊ธฐ๋กํ–ˆ์Šต๋‹ˆ๋‹ค.

Method Endpoint Status Notes
get_national_average_price() avgAllPrice.do Implemented TRADE_DT, PRICE, DIFF๋ฅผ Python ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜
get_lowest_price_top20() lowTop10.do Implemented cnt 1~20, area 2/4์ž๋ฆฌ ์‚ฌ์ „ ๊ฒ€์ฆ
search_stations_around() aroundAll.do Implemented WGS84 ์ž…๋ ฅ์€ KATEC์œผ๋กœ ๋ณ€ํ™˜ ํ›„ ์š”์ฒญ
get_station_detail() detailById.do Implemented LPG_YN์€ StationType, KPETRO_YN์€ is_kpetro
get_area_codes() areaCode.do Implemented ์ฝ”๋“œ๊ฐ’์€ ์„ ํ–‰ 0 ๋ณด์กด์„ ์œ„ํ•ด ํ•ญ์ƒ str

PDF ๊ฐ€์ด๋“œ๋ถ์˜ ์ถ”๊ฐ€ API๋Š” ์•„์ง ๊ณต์‹ ๋ช…์„ธ ํŽ˜์ด์ง€๊ฐ€ ์—†์œผ๋ฏ€๋กœ opinet.experimental์— ๋‘ก๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์ „์—๋Š” ์•ˆ์ • API๋กœ ์Šน๊ฒฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

2026-05-15 ๋””๋ฒ„๊ทธ UI/fixture ์ง€์›

REST API ๋””๋ฒ„๊ทธ UI ์„ค๊ณ„์•ˆ์— ๋งž์ถฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ณธ์ฒด์— Streamlit ๋น„์˜์กด helper๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜์—ญ ๊ตฌํ˜„
API ์นดํƒˆ๋กœ๊ทธ src/opinet/catalog.py์˜ get_api_catalog(), get_api_catalog_item(), get_api_catalog_options()
Async/httpx OpinetConfig, AsyncHttpxTransport, OpinetClient, AsyncTokenBucket
๋ฐ์ดํ„ฐ์…‹ ํ‘œ์‹œ ApiCatalogItem.dataset_name์œผ๋กœ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ๋ฐ์ดํ„ฐ์…‹๋ช… ์ œ๊ณต
์„œ๋น„์Šคํ‚ค ๋งํฌ ๋ชจ๋“  ApiCatalogItem.service_key_url์ด ์˜คํ”ผ๋„ท ์ธ์ฆํ‚ค ๋ฐœ๊ธ‰ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€๋ฆฌํ‚ด
์„œ๋น„์Šคํ‚ค ์ •๊ทœํ™” OpinetClient๊ฐ€ ๋ช…์‹œ ์ธ์ž/ํ™˜๊ฒฝ๋ณ€์ˆ˜/.env์—์„œ ํ‚ค๋ฅผ ์ฝ๊ณ  ๊ณต๋ฐฑ ๋ฌธ์ž๋ฅผ ์ œ๊ฑฐ
DebugRun src/opinet/debug.py์˜ DebugRun์ด input, request, response, parsed, processed, trace, catalog item, error๋ฅผ ๋ณด๊ด€
fixture ์ €์žฅ save_fixture(), save_debug_fixture()๊ฐ€ tests/fixtures/{function}/{case}.json ํ˜•์‹์œผ๋กœ ์ €์žฅ
replay ํ…Œ์ŠคํŠธ tests/test_generated_fixtures.py๊ฐ€ ํ•˜์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ fixture๋ฅผ ๋„คํŠธ์›Œํฌ ์—†์ด ์žฌํŒŒ์‹ฑ/๋น„๊ต
์˜ˆ์ œ UI examples/streamlit_debug_ui.py๊ฐ€ ์นดํƒˆ๋กœ๊ทธ ๊ธฐ๋ฐ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ํผ, ์„œ๋น„์Šคํ‚ค ๋งํฌ, 6๊ฐœ ๊ณ ์ • ํƒญ(Raw Response/Pydantic Model/Processed Result/Validation Errors/Debug Trace/Fixture)์„ ๊ฒ€์ฆํ•˜๋Š” ์ฐธ๊ณ  ์•ฑ

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋Ÿฐํƒ€์ž„ ์˜์กด์„ฑ์—๋Š” Streamlit์„ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด ์•ฑ์€ pip install -e ".[debug-ui]"(pyproject.toml์˜ debug-ui extra)๋กœ ์„ ํƒ์ ์œผ๋กœ ์„ค์น˜ํ•ด examples/streamlit_debug_ui.py๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ์›์น™

  • ์ธ์ฆ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” certkey, ์ถœ๋ ฅ ํฌ๋งท์€ out=json์ž…๋‹ˆ๋‹ค.
  • ์„œ๋น„์Šคํ‚ค๋Š” OpinetClient(api_key=...), OPINET_API_KEY, ํ˜„์žฌ ์ž‘์—… ๋””๋ ‰ํ„ฐ๋ฆฌ ๋˜๋Š” ๋ถ€๋ชจ ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ .env ์ˆœ์„œ๋กœ ์ฝ์Šต๋‹ˆ๋‹ค. ๋ณต์‚ฌ/๋ถ™์—ฌ๋„ฃ๊ธฐ ์ค‘ ์„ž์ธ ๊ณต๋ฐฑ ๋ฌธ์ž๋Š” ๊ฐ’์ด ์•„๋‹ˆ๋ฏ€๋กœ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
  • HTTP status/body error mapping lives in the httpx-based src/opinet/_http.py.
  • ๊ณต๊ฐœ HTTP/API๋Š” config + native async OpinetClient/AsyncHttpxTransport + ๊ณตํ†ต AsyncTokenBucket์œผ๋กœ ๊ตฌ์„ฑํ•œ๋‹ค.
  • The normalized station detail contract consumed by python-krtour-map is kept stable around provider_station_id, provider_station_name, lon/lat, katec_x/katec_y, prices, and raw.
  • ์—”๋“œํฌ์ธํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ ์˜ค๋ฅ˜๋Š” HTTP ํ˜ธ์ถœ ์ „์— OpinetInvalidParameterError๋กœ ์‹คํŒจ์‹œํ‚ต๋‹ˆ๋‹ค.
  • API ์‘๋‹ต์€ ๋ชจ๋ธ ์ƒ์„ฑ ์ „์— date, time, float, bool, StrEnum์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • AREA_CD, SIGUNCD, UNI_ID, ์ œํ’ˆ ์ฝ”๋“œ, ์ƒํ‘œ ์ฝ”๋“œ๋Š” ์ •์ˆ˜๋กœ ๋ณ€ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • KATEC ๋ณ€ํ™˜์€ src/opinet/coords.py์—์„œ pyproj๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ์ฃผ์œ ์†Œ ์ขŒํ‘œ/์ง€์—ญ ๋งคํ•‘ ๊ด€๋ จ ํƒ€์ž…๊ณผ ๋ณ€ํ™˜ ๋กœ์ง์€ ์ด ์ €์žฅ์†Œ ์•ˆ์—์„œ ์ง์ ‘ ์†Œ์œ ํ•ฉ๋‹ˆ๋‹ค. ์–‡์€ wrapper๋‚˜ mirror dataclass๋Š” ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • SIGUNCD๋Š” ์˜คํ”ผ๋„ท ์ž์ฒด 4์ž๋ฆฌ ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ์ด๋ฉฐ ๋ฒ•์ •๋™ ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ๋‚˜ 10์ž๋ฆฌ ๋ฒ•์ •๋™์ฝ”๋“œ๋กœ ์ถ”์ • ๋ณ€ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•˜๋ฉด src/opinet/vworld.py์—์„œ vworld.VworldClient.search_district(..., category="L2") ๊ฒฐ๊ณผ์˜ 5์ž๋ฆฌ id์™€ title์„ ๋ช…์‹œ ๋งค์นญํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฌธ์„œ์—์„œ ํŒŒ์ผ ์œ„์น˜๋Š” ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ๋กœ ํ‘œ๊ธฐํ•ฉ๋‹ˆ๋‹ค.
  • Python ๋‚ด๋ถ€ ๋ฌธ์„œ(docstring๊ณผ ์œ ์ง€๋ณด์ˆ˜์šฉ ์ฃผ์„)๋Š” ํ•œ๊ธ€๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์™ธ๋ถ€ API ๊ณ ์œ  ๋ช…์นญ๊ณผ ์ฝ”๋“œ ์‹๋ณ„์ž๋Š” ์›๋ฌธ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด Windows/PowerShell ํ™˜๊ฒฝ์—์„œ๋Š” rg๊ฐ€ ์‹คํ–‰ ๊ถŒํ•œ ๋ฌธ์ œ๋กœ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์‹คํŒจ ์‹œ git ls-files, Get-ChildItem -Recurse -File, Select-String์œผ๋กœ ํŒŒ์ผ ๋ชฉ๋ก๊ณผ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•œ๊ธ€ ๋ฌธ์„œ/์†Œ์Šค ํŒŒ์ผ ํ™•์ธ ์‹œ Get-Content -Encoding utf8 ๋˜๋Š” Get-Content -Raw -Encoding utf8์„ ์‚ฌ์šฉํ•ด ๊นจ์ง„ ์ถœ๋ ฅ์œผ๋กœ ์ธํ•œ ์˜คํŒ์„ ํ”ผํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ๋งคํŠธ๋ฆญ์Šค

๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ๋Š” ๋„คํŠธ์›Œํฌ ์—†์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

Area Files Coverage Intent
Type conversion tests/test_convert.py ๋‚ ์งœ/์‹œ๊ฐ„/์ˆซ์ž/๋ถˆ๋ฆฌ์–ธ/๊ณต๋ฐฑ ์ฒ˜๋ฆฌ
Code mappings tests/test_codes.py, tests/test_vworld_sigungu.py Opinet โ†” BJD ์‹œ๋„ ๋งคํ•‘, ์•Œ๋œฐ ์ƒํ‘œ ํŒ์ •, VWorld ์‹œ๊ตฐ๊ตฌ ๋งคํ•‘
Coordinates tests/test_client_endpoints.py WGS84โ†”KATEC ๋ณ€ํ™˜๊ณผ ์˜คํ”ผ๋„ท ์š”์ฒญ/์‘๋‹ต ๊ฒฝ๊ณ„
HTTP errors tests/test_http.py ์ธ์ฆ/์ฟผํ„ฐ/5xx/๋„คํŠธ์›Œํฌ/JSON ์˜ค๋ฅ˜
Async client tests/test_async_client.py OpinetClient, async context manager, TPS, ์„ธ์…˜ ์†Œ์œ ๊ถŒ
Endpoints tests/test_client_endpoints.py ๊ณต์‹ 5๊ฐœ API์˜ ํƒ€์ž…, ํŒŒ๋ผ๋ฏธํ„ฐ, ๋นˆ ๊ฒฐ๊ณผ, ๋‹จ์ผ dict ์‘๋‹ต
API catalog/debug tests/test_catalog.py, tests/test_debug.py, tests/test_generated_fixtures.py ์นดํƒˆ๋กœ๊ทธ export, DebugRun trace, ๋ฏผ๊ฐ์ •๋ณด ๋งˆ์Šคํ‚น, fixture replay
Experimental boundary tests/test_experimental.py ๋ฏธ๊ฒ€์ฆ API๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ unimplemented์ž„์„ ๊ณ ์ •

ํ•„์ˆ˜ ๊ฒ€์ฆ ๋ช…๋ น:

python -m compileall src/opinet tests
python -m pytest
python -m pytest --cov=opinet --cov-fail-under=90
python -m mypy src/opinet

Live API ํ…Œ์ŠคํŠธ ์ •์ฑ…

์‹ค์ œ ์˜คํ”ผ๋„ท ํ˜ธ์ถœ์€ ๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ์— ๋„ฃ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • OPINET_API_KEY๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ๋™์ž‘ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ์—๋Š” @pytest.mark.live๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค.
  • ์บก์ฒ˜ํ•œ fixture์—๋Š” ํ‚ค, ๊ฐœ์ธ ์‹๋ณ„ ์ •๋ณด, ํ˜ธ์ถœ URL์˜ ์ธ์ฆ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๋ผ์ด๋ธŒ ํ˜ธ์ถœ๋กœ ํ™•์ธํ•œ ์‘๋‹ต๋„ fixture์— ์ €์žฅํ•  ๋•Œ ์ˆซ์ž/๋‚ ์งœ/์‹œ๊ฐ„ ํ•„๋“œ๋Š” ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋‘ก๋‹ˆ๋‹ค.
  • .env๋Š” ๋กœ์ปฌ ์ „์šฉ์ด๋ฉฐ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. .env.example๋งŒ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.
  • areaCode.do์ฒ˜๋Ÿผ ํ•ญ์ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด์•ผ ํ•  ์—”๋“œํฌ์ธํŠธ๊ฐ€ ๋นˆ RESULT.OIL์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ‚ค๊ฐ€ ๊ณต์‹ open API ๊ฒŒ์ดํŠธ์›จ์ด์— provision๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ๊ฐ„์ฃผํ•˜๊ณ  ํŒŒ์‹ฑ smoke ํ…Œ์ŠคํŠธ๋ฅผ skipํ•ฉ๋‹ˆ๋‹ค.

๋ผ์ด๋ธŒ ํ…Œ์ŠคํŠธ ์‹คํ–‰:

pytest -m live --run-live

2026-05-01 Live Run Note

๋กœ์ปฌ .env์˜ ํ‚ค๋กœ ์‹ค์ œ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ–ˆ์„ ๋•Œ areaCode.do์˜ JSON envelope๋Š” ์ •์ƒ ์ˆ˜์‹ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ ๊ณต์‹ 5๊ฐœ API๊ฐ€ ๋ชจ๋‘ HTTP 200๊ณผ ๋นˆ RESULT.OIL์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ƒํƒœ๊ฐ€ ๊ด€์ฐฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ์‘๋‹ต์€ ํŒŒ์„œ ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹ˆ๋ผ ํ‚ค๊ฐ€ ๊ณต์‹ open API ๊ฒŒ์ดํŠธ์›จ์ด์— ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ ๊ถŒํ•œ์œผ๋กœ provision๋˜์ง€ ์•Š์€ ์ƒํƒœ์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ live ํ…Œ์ŠคํŠธ๋Š” ๋‘ ๋‹จ๊ณ„๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค.

  • ์„œ๋ฒ„ ์—ฐ๊ฒฐ๊ณผ RESULT.OIL envelope ํ™•์ธ์€ pass/fail๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
  • ์‹ค์ œ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ smoke๋Š” areaCode.do๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด skipํ•˜๊ณ , ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ˜ํ™˜๋˜๋Š” ํ‚ค์—์„œ๋Š” ๊ณต์‹ 5๊ฐœ API ์ „์ฒด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ณต ์‹ค์ˆ˜ ๋ฐฉ์ง€

์ด๋ฏธ ํ•œ ๋ฒˆ ํ™•์ธํ•œ ํ˜ผ๋™ ์ง€์ ์ž…๋‹ˆ๋‹ค.

  • StationDetail.tel์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. phone ํ•„๋“œ๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • OilPrice์—๋Š” product_name์ด ์—†์Šต๋‹ˆ๋‹ค. OIL_PRICE ์‘๋‹ต์—๋Š” PRODNM์ด ์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • RESULT.OIL๊ณผ OIL_PRICE๋Š” list๋ฟ ์•„๋‹ˆ๋ผ ๋‹จ์ผ dict๋กœ ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์‹ค์ œ ์‘๋‹ต์˜ ์ƒํ‘œ ํ•„๋“œ๋Š” POLL_DIV_CO, GPOLL_DIV_CO๊ฐ€ ์šฐ์„ ์ž…๋‹ˆ๋‹ค. *_CD๋Š” fallback์ž…๋‹ˆ๋‹ค.
  • ๊ณต๋ฐฑ 1์ž(" ")๋Š” ์˜๋ฏธ ์žˆ๋Š” ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ผ None์ž…๋‹ˆ๋‹ค.
  • ์ขŒํ‘œ ๋ณ€ํ™˜ ์ •๋ฐ€๋„์™€ ์˜คํ”ผ๋„ท ๋ชจ๋ธ ๊ฒฝ๊ณ„๋Š” python-opinet-api ํ…Œ์ŠคํŠธ๊ฐ€ ์†Œ์œ ํ•ฉ๋‹ˆ๋‹ค.
  • Unit HTTP mocks replay httpx calls with respx.

๋‹ค์Œ ์ž‘์—… ๊ธฐ์ค€

์ƒˆ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ:

  1. opinet-api.md ๋˜๋Š” ์‹ค์ œ ์‘๋‹ต์œผ๋กœ ํ•„๋“œ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ณต์‹ 5๊ฐœ์— ์†ํ•˜์ง€ ์•Š์œผ๋ฉด opinet.experimental์— ๋‘ก๋‹ˆ๋‹ค.
  3. ๋ชจ๋ธ ํ•„๋“œ๋Š” Python ๋„ค์ดํ‹ฐ๋ธŒ ํƒ€์ž…์œผ๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  4. fixture๋Š” ์‹ค์ œ ์‘๋‹ต์ฒ˜๋Ÿผ ๋ฌธ์ž์—ด ๊ฐ’์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  5. happy path, empty result, error response, ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  6. README์™€ ์ด ๋ฌธ์„œ๋ฅผ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.

๊ณต์šฉ normalized layer ํ˜„ํ™ฉ

2026-05-06 ๊ธฐ์ค€์œผ๋กœ ๊ธฐ์กด public API๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ optional/additive normalized layer๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด layer๋Š” OpiNet provider row๋ฅผ ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฐ”๋กœ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ณต์šฉ ํ•ด์„ ๊ณ„์ธต์ด๋ฉฐ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋ณ„ DB ์ €์žฅ, ETL cache, serving table, ์„œ๋น„์Šค enum ์ •์ฑ…์€ ํฌํ•จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€๋œ public surface

์˜์—ญ ๊ตฌํ˜„
canonical ์œ ์ข… FuelType ๋ฐ CanonicalFuelType alias
์œ ์ข… mapping product_code_to_fuel_type(), fuel_type_to_product_code()
์ขŒํ‘œ ํ•„๋“œ lon, lat, katec_x, katec_y, lon_lat, katec_xy
raw payload AvgPrice.raw, Station.raw, StationDetail.raw, OilPrice.raw, AreaCode.raw
Station product context Station.product_code, Station.product_name
Station trade context Station.trade_date, Station.trade_time
Station normalized helpers provider_station_id, provider_product_code, provider_product_name, fuel_type, brand_code, lon_lat, katec_xy
AvgPrice normalized helpers provider_product_code, provider_product_name, fuel_type
OilPrice helper provider_product_code, fuel_type
AreaCode helpers code_level, parent_sido_code, bjd_sido_prefix

ํ˜ธํ™˜์„ฑ ์›์น™

  • ๊ธฐ์กด dataclass ํ•„๋“œ๋Š” ์ˆœ์„œ๋ฅผ ์œ ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ƒˆ ์„ ํƒ ํ•„๋“œ๋Š” ๊ธฐ์กด ํ•„๋“œ ๋’ค์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ชจ๋“  model์˜ raw๋Š” ๋งˆ์ง€๋ง‰ dataclass ํ•„๋“œ์ž…๋‹ˆ๋‹ค.
  • raw๋Š” ์ฝ๊ธฐ ์ „์šฉ mapping์ด๋ฉฐ, nested OIL_PRICE๋Š” tuple๊ณผ read-only mapping์œผ๋กœ ๊ฐ€๋Šฅํ•œ ํ•œ ์›ํ˜• ๋ฌธ์ž์—ด ๊ฐ’์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.
  • certkey, api_key, authorization, x-api-key ๊ฐ™์€ ์ธ์ฆ ๊ด€๋ จ key๋Š” raw์—์„œ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค.

request context ๊ทœ์น™

lowTop10.do์™€ aroundAll.do์˜ Station row์—๋Š” PRODCD/PRODNM์ด ์—†๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.

  • row์— PRODCD๊ฐ€ ์žˆ์œผ๋ฉด ์‘๋‹ต ๊ฐ’์„ Station.product_code๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • row์— PRODCD๊ฐ€ ์—†์œผ๋ฉด ์š”์ฒญ์— ์‚ฌ์šฉํ•œ prodcd๋ฅผ Station.product_code์— ์ฑ„์›๋‹ˆ๋‹ค.
  • row์— PRODNM์ด ์žˆ์œผ๋ฉด Station.product_name์— ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.
  • row์— TRADE_DT/TRADE_TM์ด ์žˆ์œผ๋ฉด Station.trade_date/Station.trade_time์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด product_name, trade_date, trade_time์€ None์ž…๋‹ˆ๋‹ค.

๊ฒ€์ฆ ์ผ€์ด์Šค

tests/test_normalized_models.py์— fixture ๊ธฐ๋ฐ˜ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ผ€์ด์Šค ๊ฒ€์ฆ
ProductCode โ†” FuelType gasoline, premium_gasoline, diesel, lpg, kerosene roundtrip
mapping ์‹คํŒจ ์•Œ ์ˆ˜ ์—†๋Š” product code, ์•Œ ์ˆ˜ ์—†๋Š” fuel type, FuelType.UNKNOWN ์—ญ๋ณ€ํ™˜ ์‹คํŒจ
AreaCode 01, 0113, 001, ๋ฏธํ™•์ธ ์‹œ๋„ code
BJD prefix AreaCode.bjd_sido_prefix๊ฐ€ opinet_sido_to_bjd ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘
์ขŒํ‘œ value object ๊ธฐ์กด float ํ•„๋“œ ์œ ์ง€ ๋ฐ Station.coordinates ์ œ๊ณต
Station request product lowTop10.do, aroundAll.do ์š”์ฒญ prodcd๊ฐ€ Station์— ๋ณด์กด
Station response product ์‘๋‹ต PRODCD๊ฐ€ ์žˆ์œผ๋ฉด ์š”์ฒญ ๊ฐ’๋ณด๋‹ค ์šฐ์„ 
Station trade context ์‘๋‹ต TRADE_DT/TRADE_TM ๋ฌธ์ž์—ด์„ date/time์œผ๋กœ ๋ณ€ํ™˜
raw payload ํƒ€์ž… ๋ณ€ํ™˜ ์ „ ๋ฌธ์ž์—ด ๊ฐ’ ๋ณด์กด, top-level ๋ฐ nested raw read-only ํ™•์ธ
raw shape mapping์ด ์•„๋‹Œ raw payload๋Š” ๋ช…์‹œ์ ์œผ๋กœ ์‹คํŒจ

์‚ฌ์šฉ ์˜ˆ์‹œ

import asyncio
from opinet import OpinetClient, ProductCode


async def main() -> None:
    async with OpinetClient() as client:
        station = (await client.search_stations_around(
            lon=127.0276,
            lat=37.4979,
            prodcd=ProductCode.DIESEL,
        ))[0]

        record = {
            "provider_station_id": station.provider_station_id,
            "provider_product_code": station.provider_product_code,
            "fuel_type": station.fuel_type.value,
            "brand_code": station.brand_code,
            "price": station.price,
            "trade_date": station.trade_date,
            "trade_time": station.trade_time,
            "lon": station.coordinates.wgs84.lon,
            "lat": station.coordinates.wgs84.lat,
            "raw_price": station.raw.get("PRICE"),
        }


asyncio.run(main())

์ด ์˜ˆ์‹œ๋Š” python-opinet-api์˜ normalized ๋ชจ๋ธ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ schema, cache ์ „๋žต, raw ๋ณด๊ด€ ์—ฌ๋ถ€๋Š” ํ˜ธ์ถœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ฑ…์ž„์ž…๋‹ˆ๋‹ค.


normalized DTO record layer

2026-05-06์— opinet.normalized ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ชจ๋“ˆ์€ ๊ธฐ์กด ์‘๋‹ต ๋ชจ๋ธ์˜ ์†์„ฑ ์œ„์— ์•ฑ ์นœํ™”์ ์ธ Pydantic DTO๋ฅผ ์–น๋Š” ๊ณ„์ธต์ž…๋‹ˆ๋‹ค. ๊ธฐ์กด public model๊ณผ client method๋Š” ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ , AvgPrice.to_normalized(), Station.to_normalized(), AreaCode.to_normalized()๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

DTO๋Š” Pydantic v2 BaseModel ๊ธฐ๋ฐ˜์ด๋ฉฐ frozen=True, extra="forbid" ์„ค์ •์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ˜ธ์ถœ ์•ฑ์€ model_dump() ๋˜๋Š” model_dump(mode="json")๋กœ ์ €์žฅ ๊ณ„์ธต์— ๋„˜๊ธธ payload๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

DTO classes

DTO Source model ๊ธฐ๋ณธ endpoint ์ฃผ์š” ํ•„๋“œ
NormalizedFuelAverage AvgPrice avgAllPrice.do provider, provider_endpoint, provider_product_code, provider_product_name, fuel_type, trade_date, price, diff, raw
NormalizedFuelStation Station ํ˜ธ์ถœ์ž๊ฐ€ ์ง€์ • provider_station_id, provider_station_name, provider_product_code, provider_product_name, fuel_type, brand_code, price, distance_m, ์ฃผ์†Œ, ์ขŒํ‘œ, ๊ฑฐ๋ž˜์‹œ๊ฐ, raw
NormalizedFuelRegionCode AreaCode areaCode.do provider_region_code, provider_region_name, code_level, parent_sido_code, bjd_sido_prefix, raw

๋ชจ๋“  DTO์˜ provider ๊ฐ’์€ "opinet"์ž…๋‹ˆ๋‹ค. Station DTO์˜ provider_endpoint๋Š” lowTop10.do์™€ aroundAll.do ์ค‘ ํ˜ธ์ถœ ๋งฅ๋ฝ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๋ฏ€๋กœ ๋ณ€ํ™˜ ์‹œ ๋ช…์‹œํ•˜๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค.

datetime helpers

  • NormalizedFuelAverage.price_datetime(tz="Asia/Seoul"): trade_date๋ฅผ ํ•ด๋‹น timezone์˜ ์ž์ •์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • NormalizedFuelAverage.price_timestamp(tz="Asia/Seoul"): ์œ„ datetime์˜ Unix timestamp๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • NormalizedFuelStation.trade_datetime(tz="Asia/Seoul"): trade_date์™€ trade_time์ด ๋ชจ๋‘ ์žˆ์„ ๋•Œ timezone-aware datetime์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์—†์œผ๋ฉด None์ž…๋‹ˆ๋‹ค.
  • ๊ธฐ์กด AvgPrice.price_datetime(), AvgPrice.price_timestamp(), Station.trade_datetime()๋„ ๋™์ผํ•œ helper๋กœ ์—ฐ๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

raw JSON-safe helper

to_json_safe_raw()์™€ alias raw_to_json_safe()๋Š” read-only MappingProxyType, tuple๋กœ ๋ณด์กด๋œ nested OIL_PRICE, enum, date/time ๊ฐ’์„ JSON-safe plain dict/list ๊ตฌ์กฐ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ fixture ๊ฒฝ๋กœ์—์„œ๋Š” ์ˆซ์ž/๋‚ ์งœ/์‹œ๊ฐ„ raw ๊ฐ’์ด ์›๋ณธ ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค.

from opinet.normalized import to_json_safe_raw

raw = to_json_safe_raw(station.raw)
# json.dumps(raw, ensure_ascii=False) ๊ฐ€๋Šฅ

์ถ”๊ฐ€ ๊ฒ€์ฆ

tests/test_normalized_records.py๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ผ€์ด์Šค ๊ฒ€์ฆ
normalized record ์ƒ์„ฑ ์„ธ DTO์˜ provider/endpoint/provider code/name/fuel field ํ™•์ธ
AvgPrice KST timestamp ๋‚ ์งœ-only ํ‰๊ท ๊ฐ€๊ฐ€ Asia/Seoul ์ž์ • datetime/timestamp๋กœ ๋ณ€ํ™˜๋จ
Station trade datetime trade field ์—†์Œ์€ None, TRADE_DT/TRADE_TM ์žˆ์Œ์€ KST datetime
AreaCode normalized code level, parent sido, BJD sido prefix ํฌํ•จ
raw JSON-safe nested OIL_PRICE๊ฐ€ plain list/dict๋กœ ๋ณ€ํ™˜๋˜๊ณ  json.dumps ๊ฐ€๋Šฅ
PRODNM ์—†๋Š” Station ์š”์ฒญ prodcd๋Š” product code๋กœ ๋ณด์กด๋˜๊ณ  provider product name์€ None
Pydantic ๋™์ž‘ DTO๊ฐ€ BaseModel์ด๋ฉฐ frozen/extra forbid/model_dump ๋™์ž‘

PEP 561 typed package

2026-05-06์— PEP 561 typed package ์ง€์›์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • src/opinet/py.typed marker๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • pyproject.toml์˜ setuptools package data์— py.typed๋ฅผ ๋“ฑ๋กํ–ˆ์Šต๋‹ˆ๋‹ค.
  • dev extra์— packaging smoke์šฉ build, wheel, setuptools>=68๋ฅผ ๋ช…์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.
  • tests/test_pep561_packaging.py์—์„œ wheel/sdist๋ฅผ ๋นŒ๋“œํ•˜๊ณ , ๊ฐ ์‚ฐ์ถœ๋ฌผ์„ ์ž„์‹œ venv์— ์„ค์น˜ํ•œ ๋’ค import opinet, import opinet.normalized, downstream mypy reveal smoke๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

์ด ํ…Œ์ŠคํŠธ๋Š” Pydantic DTO์™€ ๊ธฐ์กด public export๊ฐ€ ์„ค์น˜ ํ™˜๊ฒฝ์—์„œ๋„ typed package๋กœ ๋ณด์ด๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

๋น„๋™๊ธฐ ํ˜ธ์ถœ๊ณผ ์š”์ฒญ ์†๋„

OpinetClient์˜ ์กฐํšŒ์™€ ๋””๋ฒ„๊ทธ๋Š” await, iter_stations_in_bbox๋Š” async for, ์ข…๋ฃŒ๋Š” async with ๋˜๋Š” await client.aclose()๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. Async ์ ‘๋‘์‚ฌ ํด๋ผ์ด์–ธํŠธ, aio ํŒฉํ„ฐ๋ฆฌ์™€ ๋™๊ธฐ transport๋Š” ์ œ๊ฑฐํ–ˆ๋‹ค. ์ฝ”๋“œํ‘œยท์ขŒํ‘œยท๋ชจ๋ธ ๋ณ€ํ™˜ยทfixture ์ €์žฅ ๊ฐ™์€ ๋กœ์ปฌ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋Š” ์ผ๋ฐ˜ ํ•จ์ˆ˜๋‹ค. ๊ธฐ์กด ์—”๋“œํฌ์ธํŠธ ์ธ์ž์™€ ๋ฐ˜ํ™˜ ๋ชจ๋ธ์€ ์œ ์ง€ํ•œ๋‹ค.

๊ธฐ๋ณธ max_rps=5.0์ด๋‹ค. AsyncTokenBucket(max_rps, capacity=...)๋ฅผ rate_limiter=์— ์ฃผ์ž…ํ•˜๋ฉด ์—ฌ๋Ÿฌ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ฐ™์€ ์š”์ฒญ ์˜ˆ์‚ฐ์„ ์“ด๋‹ค. ์ฃผ์ž…๋œ ๋ฒ„ํ‚ท์ด max_rps๋ณด๋‹ค ์šฐ์„ ํ•œ๋‹ค. ๊ธฐ๋ณธ capacity๋Š” max(1, max_rps)์ด๋ฉฐ ์ดˆ๊ธฐ์—๋Š” ๊ฐ€๋“ ์ฐจ ์žˆ์œผ๋ฏ€๋กœ burst๋ฅผ ํ—ˆ์šฉํ•œ๋‹ค. ์ผ์ •ํ•œ ๊ฐ„๊ฒฉ์€ capacity=1๋กœ ์„ค์ •ํ•œ๋‹ค. ๊ฐ ์š”์ฒญยท์žฌ์‹œ๋„ยท๋ฆฌ๋‹ค์ด๋ ‰ํŠธยท๋””๋ฒ„๊ทธยท๊ฒฉ์ž ์…€์— ๊ฐ™์€ ๋ฒ„ํ‚ท์„ ์ ์šฉํ•œ๋‹ค. ์ธ์ž ๊ฒ€์ฆ ์‹คํŒจ์™€ ์บ์‹œ ์ ์ค‘์€ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค. TPS๋Š” ์ผ์ผ ์ฟผํ„ฐ๋ฅผ ๋Œ€์‹ ํ•˜์ง€ ์•Š๋Š”๋‹ค.

๋ฒ„ํ‚ท์€ ํ•œ ์ด๋ฒคํŠธ ๋ฃจํ”„์—์„œ ์‚ฌ์šฉํ•œ๋‹ค. ๋Œ€๊ธฐ ์ทจ์†Œ๋Š” ํ† ํฐ์„ ์†Œ๋น„ํ•˜์ง€ ์•Š๊ณ  ๋‹ค์Œ ๋Œ€๊ธฐ์ž๋ฅผ ์ง„ํ–‰์‹œํ‚จ๋‹ค. 401/403/429๋Š” ์ฆ‰์‹œ ์‹คํŒจํ•˜๋ฉฐ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜์™€ 5xx๋งŒ ๊ธฐ์กด backoff๋กœ ์žฌ์‹œ๋„ํ•œ๋‹ค. ์‚ฌ์šฉ์ž ์ •์˜ ์ธ์ฆ/transport ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ถ”๊ฐ€ ์ „์†ก์€ ๊ณ„์ธก ๋ฒ”์œ„ ๋ฐ–์ด๋‹ค.

๋‚ด๋ถ€ HTTP ์„ธ์…˜์€ ์ฒซ ์š”์ฒญ ์‹œ ์ƒ์„ฑํ•˜๊ณ  ์ข…๋ฃŒ ์‹œ ๋‹ซ๋Š”๋‹ค. session์— ์ฃผ์ž…ํ•˜๋Š” ๋น„๋™๊ธฐ ์„ธ์…˜์€ ํ˜ธ์ถœ์ž๊ฐ€ ๋‹ซ๋Š”๋‹ค. ์ข…๋ฃŒํ•œ ํด๋ผ์ด์–ธํŠธ์˜ ์ถ”๊ฐ€ ์š”์ฒญ์€ ์‹คํŒจํ•œ๋‹ค. ๋””๋ฒ„๊ทธ ๊ธฐ๋ก์€ ContextVar๋กœ ํ˜ธ์ถœ๋ณ„ ๊ฒฉ๋ฆฌํ•˜๊ณ  ์„ฑ๊ณตยท์‹คํŒจยท์ทจ์†Œ ๋ชจ๋‘์—์„œ ๋ณต์›ํ•œ๋‹ค. ์‘๋‹ต ํŒŒ์‹ฑ์€ ์›๋ฌธ์œผ๋กœ ์™„๋ฃŒํ•œ ํ›„ ์ง„๋‹จ ๊ฒฐ๊ณผ์˜ ์•Œ๋ ค์ง„ ํ‚ค์™€ ์ธ์ฝ”๋”ฉ๋œ ํ‚ค๋ฅผ ๋งˆ์Šคํ‚นํ•œ๋‹ค. VWorld ์—ฐ๋™์€ ๋น„๋™๊ธฐ search_district๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์™„๋ฃŒ๋œ ์ง€์—ญ ์ฝ”๋“œ๋งŒ ์บ์‹œํ•œ๋‹ค.

์˜ˆ์ œ๋Š” docs/async-tps.md๋ฅผ ์ฐธ๊ณ ํ•œ๋‹ค.