Skip to content

Comments

feat: python-kis v3 โ€” 2-layer architecture with codegen pipeline, streaming, and service layer#85

Open
Soju06 wants to merge 47 commits intomainfrom
next/pykis-3
Open

feat: python-kis v3 โ€” 2-layer architecture with codegen pipeline, streaming, and service layer#85
Soju06 wants to merge 47 commits intomainfrom
next/pykis-3

Conversation

@Soju06
Copy link
Owner

@Soju06 Soju06 commented Feb 21, 2026

Summary

python-kis๋ฅผ v3๋กœ ์ „๋ฉด ์žฌ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค. KIS (ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ) Open API์˜ ๊ตญ๋‚ด/ํ•ด์™ธ ๊ฐ„ ๊ตฌ์กฐ์  ์ฐจ์ด(ํ•„๋“œ๋ช…, enum ์ฝ”๋“œ, ์‘๋‹ต ๊ตฌ์กฐ, ํ˜ธ์ถœ ํŒจํ„ด)๋ฅผ ๊ทน๋ณตํ•˜๊ธฐ ์œ„ํ•ด, 2-Layer Architecture ๊ธฐ๋ฐ˜์˜ ์ƒˆ๋กœ์šด SDK๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ๋ณ€๊ฒฝ

  • 2-Layer Architecture (L0 Raw โ†’ L1 Service โ†’ Public API)
  • Codegen Pipeline: KIS API ๋ฌธ์„œ์—์„œ 294๊ฐœ Pydantic ๋ชจ๋ธ์„ ์ž๋™ ์ƒ์„ฑ
  • Streaming System: ์‹ค์‹œ๊ฐ„ WebSocket ์‹œ์„ธ๋ฅผ ์ •๊ทœํ™” ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜
  • L1 Service Layer: Quote ๋„๋ฉ”์ธ vertical slice (๊ตญ๋‚ด/ํ•ด์™ธ ํ†ตํ•ฉ)
  • L0 Drift Detection: ๊ตฌ์กฐ์  ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ธฐ๋ฐ˜ ๋ณ€๋™ ๊ฐ์ง€ ์‹œ์Šคํ…œ
  • Store Module: SQLite ๊ธฐ๋ฐ˜ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
  • Async-First: unasync ๊ธฐ๋ฐ˜ sync ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ

Architecture

KIS API Response
       โ”‚
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  L0 โ€” Raw                       โ”‚  Generated Pydantic models (294 endpoints)
โ”‚  API ์›ํ˜• ๋ณด์กด                   โ”‚  codegen pipeline output
โ”‚  src/pykis/api/raw/             โ”‚  alias + extra="ignore" for forward compat
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚
          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  L1 โ€” Service                    โ”‚  Unified domain models + service logic
โ”‚  Type System (YAML)             โ”‚  โ†’ enums, model schemas, metadata
โ”‚  Service Modules (Python)       โ”‚  โ†’ mapping, orchestration, enrichment
โ”‚  src/pykis/service/             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚
          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Public API                      โ”‚  KisClient / KisAsyncClient
โ”‚  src/pykis/client/              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

์™œ 2-Layer์ธ๊ฐ€? (ADR-012)

v2 ๋ถ„์„ ๊ฒฐ๊ณผ, ๊ตญ๋‚ด/ํ•ด์™ธ ํ†ตํ•ฉ์€ ๋‹จ์ˆœ ํ•„๋“œ ๋งคํ•‘์ด ์•„๋‹Œ ์ ˆ์ฐจ์  ์„œ๋น„์Šค ๋กœ์ง์„ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค:

ํŒจํ„ด ์˜ˆ์‹œ ์„ ์–ธ์  YAML๋กœ ํ•ด๊ฒฐ?
Multi-call ์กฐํ•ฉ ํ•ด์™ธ ๋ถ„๋ด‰: ์ตœ๋Œ€ 12ํšŒ ํ˜ธ์ถœ + ์ค‘๋ณต ์ œ๊ฑฐ + ์‹œ๊ฐ„์ˆœ ์ •๋ ฌ โœ—
Requestโ†’Response ์ „์‚ฌ ์ž”๊ณ : ๊ณ„์ขŒ๋ฒˆํ˜ธ๋ฅผ request์—์„œ response์— ์ฃผ์ž… โœ—
์กฐ๊ฑด๋ถ€ ๋ผ์šฐํŒ… exchange.is_domestic โ†’ ๋‹ค๋ฅธ endpoint ํ˜ธ์ถœ โœ—
Computed properties ๋“ฑ๋ฝ๋ฅ , ์ „์ผ๋Œ€๋น„ ๋“ฑ API๋งˆ๋‹ค ๋‹ค๋ฅธ ๊ณ„์‚ฐ ๋ฐฉ์‹ โœ—

๊ฒฐ๋ก : L1 Normalize + L2 System์„ L1 Service๋กœ ํ†ตํ•ฉ. YAML์€ "what" (๋ชจ๋ธ ์ •์˜), Python์€ "how" (๋ณ€ํ™˜ ๋กœ์ง)์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.


Module Breakdown

1. Codegen Pipeline (src/codegen/, 67 files)

KIS API ๋ฌธ์„œ(YAML/MD)์—์„œ ํƒ€์ž… ์•ˆ์ „ํ•œ Pydantic ๋ชจ๋ธ์„ ์ž๋™ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

data/kis/ (API specs)
    โ”‚
    โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Smoke โ†’ Normalize   โ”‚ โ”€โ”€โ–ถ โ”‚ Validate โ†’ Augment  โ”‚
โ”‚ (parse, clean)      โ”‚     โ”‚ (type infer, enums)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                      โ”‚
                                      โ–ผ
                            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                            โ”‚ Generate             โ”‚
                            โ”‚ (Jinja templates)    โ”‚
                            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                      โ”‚
                                      โ–ผ
                            src/pykis/api/raw/
                            (294 endpoints, Pydantic models)

ํŒŒ์ดํ”„๋ผ์ธ ํ’ˆ์งˆ ๊ฒŒ์ดํŠธ:

  • smoke.errors: 0 โ€” ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์—†์Œ
  • validation.errors: 0 โ€” ํƒ€์ž…/์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ํ†ต๊ณผ
  • augment.errors: 0 โ€” enum/ํƒ€์ž… ์ถ”๋ก  ์˜ค๋ฅ˜ ์—†์Œ

2. L0 Raw Models (src/pykis/api/raw/, 294 files)

  • KIS API ์›ํ˜•์„ 1:1 ๋ณด์กดํ•˜๋Š” Pydantic ๋ชจ๋ธ
  • alias๋กœ ํ•œ๊ธ€ ์›๋ณธ ํ•„๋“œ๋ช… ์œ ์ง€, Python snake_case ์ ‘๊ทผ
  • extra="ignore"๋กœ ๋ฏธ๋ž˜ ํ•„๋“œ์— ๋Œ€ํ•œ ์ „๋ฐฉ ํ˜ธํ™˜์„ฑ
  • UUID ๊ธฐ๋ฐ˜ endpoint registry (registry.py)
  • ๊ตญ๋‚ด์ฃผ์‹, ํ•ด์™ธ์ฃผ์‹, ์„ ๋ฌผ์˜ต์…˜, ์žฅ๋‚ด์ฑ„๊ถŒ, ETF/ETN ๋“ฑ ์ „ ๋„๋ฉ”์ธ ์ปค๋ฒ„

3. Streaming System (src/pykis/streaming/, 71 files)

์‹ค์‹œ๊ฐ„ WebSocket ์‹œ์„ธ๋ฅผ 3๋‹จ๊ณ„๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค:

WebSocket Frame ("|" delimited)
       โ”‚
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Raw Parser      โ”‚  ๋ฐ”์ดํŠธ โ†’ L0 raw dataclass
โ”‚  (auto-generated)โ”‚  ์‹œ์„ธ/ํ˜ธ๊ฐ€/์ฒด๊ฒฐํ†ต๋ณด ๋ถ„๋ฅ˜
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Normalize       โ”‚  L0 โ†’ NormalizedTrade / NormalizedOrderBook
โ”‚  (frozen DC)     โ”‚  ๊ตญ๋‚ด/ํ•ด์™ธ ํ†ตํ•ฉ, UTC datetime, Decimal
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  System          โ”‚  ์ด๋ฒคํŠธ ๋””์ŠคํŒจ์น˜, ๊ตฌ๋… ๊ด€๋ฆฌ
โ”‚  (event mapper)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

NormalizedTrade (ํ†ตํ•ฉ ์‹œ์„ธ ๋ชจ๋ธ):

  • symbol, market, timestamp_utc
  • price, volume, open, high, low, close (all Decimal)
  • change, change_rate, change_sign
  • bid_price, ask_price, bid_size, ask_size

NormalizedOrderBook (ํ†ตํ•ฉ ํ˜ธ๊ฐ€ ๋ชจ๋ธ):

  • ์ตœ๋Œ€ 10๋‹จ๊ณ„ bid/ask (price + size tuple)
  • ๋งค๋„/๋งค์ˆ˜ ํ•ฉ๊ณ„

4. L1 Service Layer (src/pykis/service/, 10 files)

๊ตญ๋‚ด/ํ•ด์™ธ๋ฅผ ๋‹จ์ผ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ํ†ตํ•ฉํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.

Infrastructure

๋ชจ๋“ˆ ์—ญํ• 
enums.py ํ†ตํ•ฉ enum: Exchange (16๊ฐœ), ChangeSign (5๊ฐœ), MarketWarning (4๊ฐœ)
_enum_map.py L0โ†”L1 ์–‘๋ฐฉํ–ฅ ๋ณ€ํ™˜ ํ…Œ์ด๋ธ”
_convert.py Decimal ๋ณ€ํ™˜ ํ—ฌํผ (decimal_required, decimal_optional)
_types.py Market = Literal["domestic", "overseas"]

Exchange Enum (ํ†ตํ•ฉ ๊ฑฐ๋ž˜์†Œ)

class Exchange(StrEnum):
    # ๊ตญ๋‚ด
    KRX = "KRX"          # ํ•œ๊ตญ๊ฑฐ๋ž˜์†Œ
    NXT = "NXT"          # ๋„ฅ์ŠคํŠธ๋ ˆ์ด๋“œ

    # ํ•ด์™ธ
    NYSE = "NYSE"        # ๋‰ด์š•์ฆ๊ถŒ๊ฑฐ๋ž˜์†Œ
    NASDAQ = "NASDAQ"    # ๋‚˜์Šค๋‹ฅ
    AMEX = "AMEX"        # ์•„๋ฉ•์Šค
    TSE = "TSE"          # ๋„์ฟ„์ฆ๊ถŒ๊ฑฐ๋ž˜์†Œ
    SEHK = "SEHK"        # ํ™์ฝฉ๊ฑฐ๋ž˜์†Œ
    SSE = "SSE"          # ์ƒํ•˜์ด์ฆ๊ถŒ๊ฑฐ๋ž˜์†Œ
    SZSE = "SZSE"        # ์„ ์ „์ฆ๊ถŒ๊ฑฐ๋ž˜์†Œ
    HSX = "HSX"          # ํ˜ธ์น˜๋ฏผ์ฆ๊ถŒ๊ฑฐ๋ž˜์†Œ
    HNX = "HNX"          # ํ•˜๋…ธ์ด์ฆ๊ถŒ๊ฑฐ๋ž˜์†Œ

    # ์‹œ๊ฐ„์™ธ
    NYSE_EXTENDED = "NYSE_EXT"
    NASDAQ_EXTENDED = "NASDAQ_EXT"
    AMEX_EXTENDED = "AMEX_EXT"

    @property
    def is_domestic(self) -> bool: ...
    @property
    def is_overseas(self) -> bool: ...
    @property
    def is_extended_hours(self) -> bool: ...

Quote Domain (์ฒซ ๋ฒˆ์งธ vertical slice)

src/pykis/service/quote/
    models.py        # Quote (frozen dataclass, 17 fields)
    service.py       # get_quote() / get_quote_sync()
    _domestic.py     # ๊ตญ๋‚ด L0 โ†’ Quote ๋งคํ•‘ (Protocol ๊ธฐ๋ฐ˜)
    _overseas.py     # ํ•ด์™ธ L0 โ†’ Quote ๋งคํ•‘ (Protocol ๊ธฐ๋ฐ˜)

Quote ๋ชจ๋ธ โ€” ๊ตญ๋‚ด/ํ•ด์™ธ ํ†ตํ•ฉ ํ˜„์žฌ๊ฐ€:

@dataclass(frozen=True, slots=True)
class Quote:
    symbol: str
    exchange: Exchange
    price: Decimal              # ํ˜„์žฌ๊ฐ€
    previous_close: Decimal     # ์ „์ผ์ข…๊ฐ€
    change: Decimal             # ์ „์ผ๋Œ€๋น„
    change_rate: Decimal        # ๋“ฑ๋ฝ๋ฅ 
    change_sign: ChangeSign     # ๋“ฑ๋ฝ๋ถ€ํ˜ธ
    open: Decimal               # ์‹œ๊ฐ€
    high: Decimal               # ๊ณ ๊ฐ€
    low: Decimal                # ์ €๊ฐ€
    volume: int                 # ๋ˆ„์ ๊ฑฐ๋ž˜๋Ÿ‰
    turnover: Decimal           # ๋ˆ„์ ๊ฑฐ๋ž˜๋Œ€๊ธˆ
    previous_volume: int        # ์ „์ผ๊ฑฐ๋ž˜๋Ÿ‰
    upper_limit: Decimal | None # ์ƒํ•œ๊ฐ€ (๊ตญ๋‚ด ์ „์šฉ)
    lower_limit: Decimal | None # ํ•˜ํ•œ๊ฐ€ (๊ตญ๋‚ด ์ „์šฉ)
    market_warning: MarketWarning | None  # ์‹œ์žฅ๊ฒฝ๊ณ  (๊ตญ๋‚ด ์ „์šฉ)
    is_trading_halted: bool | None        # ๊ฑฐ๋ž˜์ •์ง€ (๊ตญ๋‚ด ์ „์šฉ)
    is_orderable: bool | None             # ์ฃผ๋ฌธ๊ฐ€๋Šฅ (ํ•ด์™ธ ์ „์šฉ)

์„œ๋น„์Šค ๋ผ์šฐํŒ… ํŒจํ„ด:

async def get_quote(client, symbol, exchange) -> Quote:
    if exchange.is_domestic:
        # L0: /uapi/domestic-stock/v1/quotations/inquire-price-2
        response = await endpoint.acall(client, {...})
        return map_domestic_quote(response.output, symbol, exchange)
    else:
        # L0: /uapi/overseas-price/v1/quotations/price
        response = await endpoint.acall(client, {...})
        return map_overseas_quote(response.output, symbol, exchange)

Protocol ๊ธฐ๋ฐ˜ ๋งคํผ โ€” L0 ์ฝ”๋“œ์™€์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ์ตœ์†Œํ™”:

class DomesticQuoteRaw(Protocol):
    """L0 ๊ตญ๋‚ด ํ˜„์žฌ๊ฐ€ ์‘๋‹ต์˜ ๊ตฌ์กฐ์  ๊ณ„์•ฝ."""
    stck_prpr: Decimal
    stck_prdy_clpr: int
    prdy_vrss: int
    prdy_ctrt: Decimal
    ...

def map_domestic_quote(raw: DomesticQuoteRaw, symbol: str, exchange: Exchange) -> Quote:
    ...

5. L0 Drift Detection (scripts/check_l0_drift.py)

codegen์ด L0 ๋ชจ๋ธ์„ ์žฌ์ƒ์„ฑํ•˜๋ฉด L1 Service์™€์˜ ์ •ํ•ฉ์„ฑ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ตฌ์กฐ์  ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋น„๊ต๋กœ ๋ณ€๋™์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ณ„์‚ฐ

def compute_endpoint_signature(endpoint) -> str:
    shape = {
        "meta": {"method", "path", "tr_id"},
        "request": extract_model_shape(request_model),
        "response": extract_model_shape(response_model),
    }
    return f"sha256:{hashlib.sha256(canonical_json).hexdigest()}"

ํ•ด์‹œ์— ํฌํ•จ (structural shape):

  • ํ•„๋“œ๋ช…, ํƒ€์ž…, alias, required, default
  • Enum ๋ฉค๋ฒ„ (name โ†’ value)
  • ์ค‘์ฒฉ ๋ชจ๋ธ (์žฌ๊ท€)
  • Endpoint meta (method, path, tr_id)

ํ•ด์‹œ์— ๋ฏธํฌํ•จ (documentation):

  • ํ•œ๊ธ€ ์„ค๋ช…, docstring, ์ฃผ์„

๋งค๋‹ˆํŽ˜์ŠคํŠธ (data/service/_l0_manifest.yaml)

endpoints:
  "995f745a-aa8a-491b-85ef-0d4697094b58":
    name: "์ฃผ์‹ํ˜„์žฌ๊ฐ€ ์‹œ์„ธ2 [v1_๊ตญ๋‚ด์ฃผ์‹-065]"
    path: "/uapi/domestic-stock/v1/quotations/inquire-price-2"
    signature: "sha256:a1b2c3..."
    consumers:
      - quote              # โ† ์ด L0๋ฅผ ์‚ฌ์šฉํ•˜๋Š” L1 ๋„๋ฉ”์ธ

  "3eeac674-072d-4674-a5a7-f0ed01194a81":
    name: "ํ•ด์™ธ์ฃผ์‹ ํ˜„์žฌ์ฒด๊ฒฐ๊ฐ€ [v1_ํ•ด์™ธ์ฃผ์‹-009]"
    path: "/uapi/overseas-price/v1/quotations/price"
    signature: "sha256:d4e5f6..."
    consumers:
      - quote

268๊ฐœ endpoint์˜ ๊ตฌ์กฐ์  ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

Pre-commit Hook ์—ฐ๋™

# .pre-commit-config.yaml
- id: l0-drift-check
  name: L0 drift detection
  entry: .venv/bin/python scripts/check_l0_drift.py
  files: (src/pykis/api/raw/|src/pykis/service/|data/service/_l0_manifest)
git commit
  โ†“
ruff โ†’ ruff-format โ†’ ty โ†’ l0-drift-check
                              โ†“
                     exit 0 โ†’ โœ… pass
                     exit 1 โ†’ โŒ block (L1 consumer ์˜ํ–ฅ)
                     exit 2 โ†’ โš ๏ธ pass (consumer ์—†๋Š” ๋ณ€๋™, warning)

6. Store Module (src/pykis/store/, 64 files)

SQLite ๊ธฐ๋ฐ˜ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ(์ข…๋ชฉ ์ฝ”๋“œ, ๊ฑฐ๋ž˜์†Œ ์ •๋ณด ๋“ฑ) ๊ด€๋ฆฌ.

7. Transport & Client (src/pykis/client/)

  • Async-first: /_async/client.py๊ฐ€ canonical source
  • Sync auto-generated: scripts/build_unasync.py๊ฐ€ /_sync/ ์ƒ์„ฑ
  • Rate limiter, token refresh, retry logic
  • asyncio.Lock โ†’ threading.Lock ์ž๋™ ๋ณ€ํ™˜

Specifications & Governance

๋ฌธ์„œ ์—ญํ• 
specs/project.md ํ”„๋กœ์ ํŠธ ์ •์˜
specs/stakeholder-requirements.md SDK ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ (SR-xxx)
specs/srs/ ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ (SyRS-xxx)
specs/architecture/layered-model.md 2-Layer ์•„ํ‚คํ…์ฒ˜
specs/architecture/adr/ADR-012-service-layer.md L1+L2 ํ†ตํ•ฉ ๊ฒฐ์ •
specs/governance/ ๊ฑฐ๋ฒ„๋„Œ์Šค ์ •์ฑ…

Agent Skills

Skill ์—ญํ• 
/service-build L1 ์„œ๋น„์Šค ๋„๋ฉ”์ธ ๊ตฌ์ถ• 5๋‹จ๊ณ„ ์›Œํฌํ”Œ๋กœ์šฐ
/pipeline-run codegen ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ + ๊ฒ€์ฆ
/override-review Override ํ›„๋ณด HITL ์Šน์ธ
/api-audit KIS API ์ŠคํŽ™-์‹ค์‘๋‹ต ์ •ํ•ฉ์„ฑ ๊ฐ์‚ฌ
/explore-kis-api KIS API ๋ ˆํผ๋Ÿฐ์Šค ํƒ์ƒ‰

Stats

์˜์—ญ ํŒŒ์ผ ์ˆ˜ ๋ณ€๊ฒฝ๋Ÿ‰
L0 Raw Models 294 +59,065
Streaming 71 +10,335
Codegen 67 +14,978
Store 64 new
L1 Service 10 +529
Specs 28 new
Data (API specs) 718 new
Tests 17 +3,465 / -261
Scripts 3 +600
์ดํ•ฉ 1,487 files +403,067 / -21,397

Test Plan

  • pytest โ€” 140 tests passed (11.66s)
  • ruff check โ€” clean
  • ty check โ€” clean
  • L0 drift detection โ€” 268 endpoints baselined, 0 drift
  • Pre-commit hooks โ€” ruff + ruff-format + ty + l0-drift-check

Next Steps (Future PRs)

  • Chart domain (multi-call orchestration, ํ•ด์™ธ 12-call ํŒจํ„ด)
  • Order + Balance domains (requestโ†’response ์ „์‚ฌ)
  • Public API surface (KisClient / KisAsyncClient)
  • Integration tests with mock server

Soju06 added 30 commits January 12, 2026 18:25
store codegen ํŒŒ์ดํ”„๋ผ์ธ์ด ํ˜„์žฌ int, Decimal ์ถ”๋ก ์ด ์ž‘๋™ํ•˜์ง€ ์•Š์Œ,

์ˆซ์žํ˜•์˜ ๊ฒฝ์šฐ codegen์—์„œ pandas ๋“ฑ์„ ์ด์šฉํ•˜์—ฌ ํ†ต๊ณ„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•„๋“œ๊ฐ€ ์ˆซ์žํ˜•์ธ์ง€ ์ถ”๋ก +ํ•„๋“œ๋ช… ํœด๋ฆฌ์Šคํ‹ฑ(api codegen๊ณผ ๋™์ผํ•œ ์•„ํ‚คํ…์ณ, ์ฝ”์–ด ์‹œ์Šคํ…œ์„ ๊ฐ€์ ธ์•ผํ•จ)

ํ•˜๊ณ , ์ถ”๋ก +ํœด๋ฆฌ์Šคํ‹ฑ์— ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ ๋กœ๊น…ํ•ด์ฃผ๋ฉด ๋‚ด๊ฐ€ ์ง์ ‘ ๊ฒ€ํ† ํ•ด๋ณผ๊ฒŒ

`AGENTS.md`

`python-kis/AGENTS.md`
ํ˜„์žฌ codegen์„ ๊ตญ๋‚ด&ํ•ด์™ธ ๊ธฐ๋ณธ ์‹œ์„ธ๋งŒ ์ƒ์„ฑํ•˜๋„๋ก ์ž„์‹œ๋กœ ์„ค์ •ํ•ด๋’€๋Š”๋ฐ, ์ด์ œ ํ•ด๋‹น ํ•„ํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ์ „์ฒด ์‹คํ–‰ ์ง„ํ–‰ํ•ด์ค˜, ๊ทธ ํ›„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ธํ•˜๊ฒŒ ์•Œ๋ ค์ค˜,

ํŠนํžˆ ๋ฌธ์„œ์˜ ์˜๋ฏธ์™€ ์ƒ์„ฑ๋œ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์˜ ํ˜•์ƒ์ด ๋‹ค๋ฅธ ๋ถ€๋ถ„์„ ์ง์ ‘ ๊ฒฐ๊ณผ๋ฌผ์„ ํ™•์ธํ•˜๋ฉฐ ์•Œ๋ ค์ค˜

์ค‘์š”ํ•œ๊ฑด FAQ, ์˜ค๋ฅ˜์ฝ”๋“œ, OAuth2์ชฝ์€ ํ˜•์ƒ์ด ๋‹ค๋ฅด๋ฏ€๋กœ ์ด๋Š” ๋ฌด์‹œํ•ด์•ผํ•ด
โ€ฆ and the next 3 tasks included.

- Created: `.cursor/plans/pykis-streaming-realtime-standardization.plan.md`
- Includes: business_date inference via local time, timezone rules, Decimal + heuristic + override logging, and the next three concrete tasks (FieldSpec, StreamingParseContext, override report format) with runnable detail.

Next step options:
1) Start Task 1 (TR inventory + field order extraction) and produce the TR list + field counts.
2) Start Task 2 (business_date/timezone policy spec) and lock the inference rules.
3) Start Task 3 (FieldSpec draft) for domestic/overseas realtime TRs.

Tell me which one to start.
All three steps completed (plus the followโ€‘through items) and the plan is updated in `/.cursor/plans/pykis-streaming-realtime-standardization.plan.md`.

What I did
- Step 1: TR inventory + field order extracted; output in `tmp/docs/reports/streaming_tr_inventory.json`.
- Step 2: Local NTP-based business_date inference policy finalized; parse-context spec in `tmp/docs/reports/streaming_parse_context_spec.yaml`.
- Step 3: FieldSpec draft finalized (L0 Decimal/UTC rules, YYMMDD policy, duplicate alias policy) in `tmp/docs/reports/streaming_fieldspec_draft.yaml`.
- Override logging format defined in `tmp/docs/reports/streaming_override_report_schema.json`.
- Codegen input schema defined in `tmp/docs/reports/streaming_codegen_input_schema.yaml`.

Key decisions captured
- business_date defaults to local NTP-synced date in market timezone when payload lacks date; explicit date wins; clock regression logs anomaly.
- YYMMDD fields use `year_policy: century_from_local`.
- HDFSASP0 duplicate fields are preserved with index-based aliases (`_2` suffix).
- ZDIV scaling applied to overseas price fields; missing ZDIV logs candidate.

If you want, I can now start the codegen design using `tmp/docs/reports/streaming_codegen_input_schema.yaml` as the source of truth.
โ€ฆuality loop. Generated realtime raw models + registry are in place, and the pipeline validates/outputs reports.

What changed
- New streaming codegen pipeline: `src/codegen/streaming/` (load spec โ†’ validate โ†’ generate โ†’ report)
- CLI support: added โ€œStreaming codegenโ€ action in `src/codegen/cli.py`
- New streaming raw package skeleton:
  - `src/pykis/streaming/raw/base.py`
  - `src/pykis/streaming/raw/specs.py`
  - `src/pykis/streaming/raw/generated/*.py` (generated models + registry)
- Reports/specs used in this run:
  - `tmp/docs/reports/streaming_fieldspec_draft.yaml`
  - `tmp/docs/reports/streaming_codegen_report.json`

Quality loop done
- Ran codegen โ†’ inspected outputs โ†’ removed perโ€‘class config (use base config) โ†’ regenerated โ†’ verified FIELD_ORDER/FIELD_ALIASES match spec
- Duplicate field handling (HDFSASP0) preserved with indexโ€‘based aliases
- YYMMDD fields carry `year_policy: century_from_local` in rules

Codegen execution
- `PYTHONPATH=src python - <<'PY' ... run_pipeline()` executed successfully

Tests
- `python -m ruff check .` โœ“
- `python -m ruff format .` โœ“
- `python -m ty check src` โœ—
  Unresolved imports in existing files (`httpx_retries`, `aiopath`). These are environment deps, not introduced by this change.

Files to review
- `src/codegen/streaming/pipeline.py`
- `src/codegen/streaming/steps/io.py`
- `src/codegen/streaming/steps/validate.py`
- `src/codegen/streaming/steps/generate.py`
- `src/pykis/streaming/raw/specs.py`
- `src/pykis/streaming/raw/generated/registry.py`
- `src/pykis/streaming/raw/generated/hdfsasp0.py` (duplicate alias example)

Next steps (pick one)
1) Wire a runtime parser that consumes `StreamingSpec` and emits L0 models with Decimal/UTC using local-time business_date policy.
2) Promote this streaming codegen into CI/build scripts (add to codegen workflow).
3) Review YYMMDD handling for `HDFSCNT0` and decide if an override is required.
์ž‘์—… ๋‚ด์šฉ (1๋ฒˆ)
- ๋Ÿฐํƒ€์ž„ ํŒŒ์„œ ์ถ”๊ฐ€: `src/pykis/streaming/raw/parser.py`
  - `parse_streaming_frame()`์ด `|` + `^` ํ”„๋ ˆ์ž„์„ ํ•ด์„ํ•ด L0 ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜
  - `FIELD_ORDER/ALIASES` ๋ฐ˜์˜, ์ค‘๋ณต ํ•„๋“œ alias ์ฒ˜๋ฆฌ
  - Decimal/UTC ํŒŒ์‹ฑ, ZDIV ์Šค์ผ€์ผ, business_date ์ถ”๋ก , edge-case ๋กœ๊น… ํฌํ•จ
- ์ปจํ…์ŠคํŠธ/ํƒ€์ž„์†Œ์Šค: `src/pykis/streaming/raw/context.py`
  - ๋กœ์ปฌ(NTP sync) ๊ธฐ์ค€ business_date ์ถ”๋ก 
  - ์‹œ๊ฐ„ ์—ญํ–‰ ๊ฐ์ง€ ์‹œ `time_backwards` ํ”Œ๋ž˜๊ทธ โ†’ ํŒŒ์„œ์—์„œ override ํ›„๋ณด ๋กœ๊ทธ
- override ๋กœ๊น… ๊ตฌ์กฐ: `src/pykis/streaming/raw/overrides.py`
  - `OverrideCandidate` + `OverrideLogger` ์ธํ„ฐํŽ˜์ด์Šค
  - ๊ธฐ๋ณธ์€ `NullOverrideLogger` (I/O ์—†์Œ, ์ฃผ์ž… ๊ฐ€๋Šฅ)
- exports ๊ฐฑ์‹ : `src/pykis/streaming/raw/__init__.py`

์‚ฌ์šฉ ์˜ˆ์‹œ
```python
from pykis.streaming.raw import RAW_REGISTRY, StreamingParseContext, parse_streaming_frame

context = StreamingParseContext(market_timezone="Asia/Seoul")
result = parse_streaming_frame(frame, registry=RAW_REGISTRY, context=context)
# result.records -> L0 ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ (Decimal/UTC ๋ณ€ํ™˜ ์™„๋ฃŒ)
```

๋ฃฐ ์ ์šฉ ์š”์•ฝ
- ๋‚ ์งœ ์—†๋Š” TR์€ local time ๊ธฐ์ค€ business_date๋กœ ๋ณด์ •
- date_only โ†’ 00:00 local โ†’ UTC ๋ณ€ํ™˜
- time_only โ†’ date_field + time โ†’ UTC ๋ณ€ํ™˜
- `FieldSource.HEURISTIC`๋Š” override ํ›„๋ณด๋กœ ๋กœ๊น…

---

YYMMDD (3๋ฒˆ) ์ƒ์„ธ ์„ค๋ช…
๋ฌธ์ œ ์ง€์ 
- `HDFSCNT0`์˜ `XYMD`, `KYMD` ๊ธธ์ด๊ฐ€ 6 โ†’ `YYMMDD`
- ๋ฌธ์„œ์ƒ `TYMD`๋Š” 8์ž๋ฆฌ(YYYYMMDD)

ํ˜„์žฌ ์ •์ฑ…(์ฝ”๋“œ/์ŠคํŽ™)
- `year_policy: century_from_local`
  - ๋กœ์ปฌ(์‹œ์žฅ ํƒ€์ž„์กด) ํ˜„์žฌ ์—ฐ๋„์˜ โ€œ์„ธ๊ธฐโ€๋ฅผ ๋ถ™์ž„
  - ์˜ˆ: local=2026 โ†’ `24xxxx`๋Š” 2024๋…„์œผ๋กœ ํŒŒ์‹ฑ
- ์žฅ์ : ๋‹จ์ˆœํ•˜๊ณ  ๋‹น์žฅ ์šด์˜์— ์‹ค์šฉ์ 
- ๋ฆฌ์Šคํฌ:
  - ์„ธ๊ธฐ ๊ฒฝ๊ณ„(2099โ†’2100)์—์„œ ์ž˜๋ชป๋  ์ˆ˜ ์žˆ์Œ
  - ์ œ๊ณต๊ธฐ๊ด€์ด ๋‹ค๋ฅธ ์„ธ๊ธฐ ๊ฐ’์„ ์“ฐ๋ฉด ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ

๋” ์•ˆ์ „ํ•œ ๊ฐœ์„  ์˜ต์…˜
1) **TYMD ๊ธฐ๋ฐ˜ ์•ต์ปค๋ง**
   - `TYMD`(YYYYMMDD)๊ฐ€ ์กด์žฌํ•˜๋ฏ€๋กœ, `XYMD/KYMD`์˜ ์—ฐ๋„๋ฅผ TYMD์˜ ์—ฐ๋„๋กœ ๋งž์ถ”๋Š” ๋ฐฉ์‹
   - ๊ฐ€์žฅ ์•ˆ์ •์ ์ด๋ฉฐ โ€œYYMMDDโ€ ์• ๋งค์„ฑ ์ œ๊ฑฐ
2) **์˜ค๋ฒ„๋ผ์ด๋“œ๋กœ ํ˜•์‹ ๊ต์ •**
   - ๋ฌธ์„œ/์ƒ˜ํ”Œ์—์„œ `XYMD/KYMD`๊ฐ€ ์‚ฌ์‹ค์ƒ 8์ž๋ฆฌ๋ผ๊ณ  ํ™•์ธ๋˜๋ฉด
     `YYMMDD โ†’ YYYYMMDD`๋กœ spec override ์ ์šฉ ๊ฐ€๋Šฅ
3) **๋ถˆ์ผ์น˜ ๊ฐ์ง€ ๋กœ๊ทธ**
   - `XYMD` + `XHMS`๊ฐ€ `TYMD`์™€ ๋‚ ์งœ๊ฐ€ ๋‹ค๋ฅด๋ฉด ํ›„๋ณด ๋กœ๊ทธ ์ƒ์„ฑ
     (์ž๋™ override ๊ธˆ์ง€ ๊ทœ์น™ ์œ ์ง€)

์›ํ•˜๋ฉด 1) TYMD ์•ต์ปค๋ง ๋กœ์ง์„ ์ถ”๊ฐ€๋กœ ๊ตฌํ˜„ํ• ๊ฒŒ.

---

QA
- `python -m ruff check src/pykis/streaming/raw src/codegen/streaming`
- `python -m ruff format src/pykis/streaming/raw src/codegen/streaming`
- `python -m ty check src/pykis/streaming src/codegen/streaming`

๋‹ค์Œ ์„ ํƒ
1) TYMD ์•ต์ปค๋ง ๋กœ์ง์„ ํŒŒ์„œ์— ์ถ”๊ฐ€ํ• ์ง€ ๊ฒฐ์ •
2) override ํ›„๋ณด ๋กœ๊ทธ๋ฅผ ํŒŒ์ผ(JSONL)๋กœ ์ž๋™ ์ ์žฌํ•˜๋Š” ๋กœ๊ฑฐ ์ถ”๊ฐ€
3) L0 ํŒŒ์„œ๋ฅผ ์ƒ์œ„ streaming transport์— ์—ฐ๊ฒฐ(๊ตฌ๋…/๋ผ์šฐํŒ… ๋‹จ๊ณ„)
์ž‘์—… ๋‚ด์šฉ
- TYMD ์•ต์ปค๋ง ๋กœ์ง ์ถ”๊ฐ€: `src/pykis/streaming/raw/parser.py`
- JSONL override ๋กœ๊ฑฐ ์ถ”๊ฐ€: `src/pykis/streaming/raw/overrides.py`
- L0 ํŒŒ์„œ โ†” transport ๋ธŒ๋ฆฟ์ง€ ์ถ”๊ฐ€: `src/pykis/streaming/raw/adapter.py`
- export ์ •๋ฆฌ: `src/pykis/streaming/raw/__init__.py`

YYMMDD ์ฒ˜๋ฆฌ(์ƒ์„ธ)
- ๋Œ€์ƒ: `HDFSCNT0`์˜ `XYMD/KYMD`๊ฐ€ 6์ž๋ฆฌ(YYMMDD)์ธ ๋ฌธ์ œ
- ๊ฐœ์„ ๋œ ์ •์ฑ…
  - `XYMD`๋Š” **TYMD(8์ž๋ฆฌ)**๊ฐ€ ์žˆ์„ ๋•Œ **TYMD์˜ ์—ฐ๋„**๋ฅผ ์•ต์ปค๋กœ ์‚ฌ์šฉ
    - ์˜ˆ: `TYMD=20250102`, `XYMD=241231` โ†’ `2024-12-31`๋กœ ํŒŒ์‹ฑ
  - `KYMD`๋Š” **Asia/Seoul ๋กœ์ปฌ ์—ฐ๋„ ๊ธฐ์ค€**(century_from_local) ์œ ์ง€
    - KST ๊ธฐ์ค€์˜ ์—ฐ๋„ ๋ณด์ •์ด ๋” ์•ˆ์ „ํ•˜๋‹ค๋Š” ํŒ๋‹จ
- ๋กœ๊น…
  - `TYMD`๊ฐ€ ์—†๊ฑฐ๋‚˜ 8์ž๋ฆฌ/์ˆซ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด `yy_anchor_invalid` ํ›„๋ณด ๋กœ๊ทธ ์ƒ์„ฑ
- ๋ณ€๊ฒฝ ์œ„์น˜
  - `_parse_date()` ๋‚ด๋ถ€์—์„œ `TYMD` ์•ต์ปค๋ฅผ ์ ์šฉ(๊ทœ์น™ timezone์ด `market`์ธ ๊ฒฝ์šฐ์—๋งŒ)

Override JSONL ๋กœ๊ฑฐ(2๋ฒˆ)
- ์ถ”๊ฐ€ ํด๋ž˜์Šค: `JsonlOverrideLogger`
- ์‚ฌ์šฉ ์˜ˆ:
```python
from pathlib import Path
from pykis.streaming.raw import JsonlOverrideLogger

logger = JsonlOverrideLogger(Path("tmp/docs/reports/streaming_overrides.jsonl"))
```
- ํฌ๋งท: JSONL (ํ•œ ์ค„๋‹น 1๊ฐœ ํ›„๋ณด), `ensure_ascii=True`๋กœ ์ €์žฅ

Transport ์—ฐ๊ฒฐ ๋ธŒ๋ฆฟ์ง€(3๋ฒˆ)
- ์ถ”๊ฐ€ ํด๋ž˜์Šค: `StreamingFrameHandler` + `StaticContextProvider`
- ์—ญํ• : transport๊ฐ€ ์ˆ˜์‹ ํ•œ **raw frame ๋ฌธ์ž์—ด์„ ๋ฐ”๋กœ L0 ๋ชจ๋ธ๋กœ ํŒŒ์‹ฑ**ํ•˜๊ณ , ์ฝœ๋ฐฑ์œผ๋กœ ์ „๋‹ฌ
- ์‚ฌ์šฉ ์˜ˆ:
```python
from pykis.streaming.raw import (
    RAW_REGISTRY,
    StaticContextProvider,
    StreamingFrameHandler,
    JsonlOverrideLogger,
)

context_provider = StaticContextProvider(
    domestic_timezone="Asia/Seoul",
    overseas_timezone="America/New_York",  # ํ•ด์™ธ ๊ธฐ๋ณธ TZ ๋ช…์‹œ ํ•„์š”
)
logger = JsonlOverrideLogger(Path("tmp/docs/reports/streaming_overrides.jsonl"))

handler = StreamingFrameHandler(
    registry=RAW_REGISTRY,
    context_provider=context_provider,
    override_logger=logger,
)

# transport์—์„œ frame ์ˆ˜์‹  ์‹œ
handler.handle_frame(frame)
```
- ์•”ํ˜ธํ™” ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ
  - `parse_streaming_frame(..., decryptor=...)` ์ง€์›
  - ์•”ํ˜ธํ™” ํ”Œ๋ž˜๊ทธ๊ฐ€ `1`์ธ๋ฐ decryptor๊ฐ€ ์—†์œผ๋ฉด ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฐœ์ƒ

ํ…Œ์ŠคํŠธ
- `python -m ruff check src/pykis/streaming/raw`
- `python -m ruff format src/pykis/streaming/raw`
- `python -m ty check src/pykis/streaming/raw`

ํ•„์š”ํ•˜๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋กœ
1) ํ•ด์™ธ ์‹œ์žฅ๋ณ„ timezone ์ž๋™ ๊ฒฐ์ •(์‹œ์žฅ์ฝ”๋“œ/์‹ฌ๋ณผ ๊ธฐ๋ฐ˜) ๋กœ์ง ์ถ”๊ฐ€
2) JSONL ๋กœ๊ฑฐ์— batch flush / buffering ์˜ต์…˜ ์ถ”๊ฐ€
3) WebSocket transport(์‹ค์ œ ์ˆ˜์‹ /๊ตฌ๋…) ๋ชจ๋“ˆ๊ณผ ์ง์ ‘ ์—ฐ๊ฒฐ
โ€ฆ(TYMD ์•ต์ปค๋ง) ์ƒ์„ธ๋ฅผ ์ •๋ฆฌํ–ˆ์–ด.

๋ฐ”๋€ ๋‚ด์šฉ
- ์‹œ์žฅ ์ฝ”๋“œ โ†’ timezone ๋งคํ•‘ ์ถ”๊ฐ€
  - `src/pykis/streaming/raw/market.py`
  - `RSYM`/`TR_KEY`์—์„œ `D/R + 3์ž๋ฆฌ ์‹œ์žฅ์ฝ”๋“œ` ์ถ”์ถœ โ†’ TZ ๋งคํ•‘
  - ๋งคํ•‘: NAS/NYS/AMS/BAQ/BAY/BAA โ†’ `America/New_York`, TSE โ†’ `Asia/Tokyo`, HKS โ†’ `Asia/Hong_Kong`, SHS/SZS โ†’ `Asia/Shanghai`, HSX/HNX โ†’ `Asia/Ho_Chi_Minh`
- L0 ํŒŒ์„œ์—์„œ TYMD ์•ต์ปค๋ง ์ ์šฉ (YYMMDD ์•ˆ์ •ํ™”)
  - `src/pykis/streaming/raw/parser.py`
  - `XYMD`๊ฐ€ `YYMMDD`์ผ ๋•Œ `TYMD` ์—ฐ๋„๋กœ ์•ต์ปค๋ง(์‹œ์žฅ timezone์ผ ๋•Œ๋งŒ)
  - `TYMD`๊ฐ€ ๋น„์ •์ƒ์ด๋ฉด override ํ›„๋ณด(`yy_anchor_invalid`) ๋กœ๊น…
- WebSocket ์—ฐ๊ฒฐ ์ง€์ (๋ผ์šฐํŒ…) ๊ตฌํ˜„
  - `src/pykis/streaming/router.py`
  - `StreamingEventRouter`๊ฐ€ `StreamingFrameHandler`๋ฅผ ์‚ฌ์šฉํ•ด ํ”„๋ ˆ์ž„ ํŒŒ์‹ฑ ํ›„ `tr_id`๋ณ„ ํ•ธ๋“ค๋Ÿฌ์— ๋ถ„๋ฐฐ
- ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ ์–ด๋Œ‘ํ„ฐ ๊ฐ•ํ™”
  - `src/pykis/streaming/raw/adapter.py`
  - `StreamingFrameHandler`๊ฐ€ payload์—์„œ RSYM ๋“ฑ ์ฝ๊ณ  timezone์„ ์‹œ์žฅ ์ฝ”๋“œ๋กœ ์žฌ์„ค์ •
  - `StaticContextProvider`๋Š” overseas ๊ธฐ๋ณธ TZ๊ฐ€ ์—†์œผ๋ฉด `UTC`๋ฅผ fallback์œผ๋กœ ์‚ฌ์šฉ (resolver๊ฐ€ ๋ฎ์–ด์”€)

์‚ฌ์šฉ ์˜ˆ์‹œ (์‹œ์žฅ์ฝ”๋“œ ๊ธฐ๋ฐ˜ + ๋ผ์šฐํŒ…)
```python
from pathlib import Path
from pykis.streaming import StreamingEventRouter
from pykis.streaming.raw import (
    RAW_REGISTRY,
    JsonlOverrideLogger,
    StaticContextProvider,
    StreamingFrameHandler,
)

context_provider = StaticContextProvider(
    domestic_timezone="Asia/Seoul",
    overseas_timezone="America/New_York",  # ๊ธฐ๋ณธ fallback
)

frame_handler = StreamingFrameHandler(
    registry=RAW_REGISTRY,
    context_provider=context_provider,
    override_logger=JsonlOverrideLogger(Path("tmp/docs/reports/streaming_overrides.jsonl")),
)

router = StreamingEventRouter(frame_handler)

def on_trade(event):
    print(event)

router.register("HDFSCNT0", on_trade)

# websocket ์ˆ˜์‹  ๋ฃจํ”„์—์„œ:
# router.handle_frame(frame)
```

YYMMDD ์ƒ์„ธ (TYMD ์•ต์ปค๋ง)
- ๋ฌธ์ œ: `HDFSCNT0`์˜ `XYMD/KYMD`๋Š” 6์ž๋ฆฌ(YYMMDD) โ†’ ์„ธ๊ธฐ ๋ชจํ˜ธ
- ํ•ด๊ฒฐ:
  - `XYMD`์˜ ์—ฐ๋„๋Š” `TYMD`(YYYYMMDD)๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ์—ฐ๋„๋ฅผ ์•ต์ปค๋กœ ์‚ฌ์šฉ
    โ†’ `TYMD=20250102`, `XYMD=241231` โ†’ 2024-12-31
  - `KYMD`๋Š” KST ๋กœ์ปฌ ์„ธ๊ธฐ ๊ธฐ์ค€(century_from_local) ์œ ์ง€
- ๋กœ๊น…:
  - `TYMD`๊ฐ€ 8์ž๋ฆฌ/์ˆซ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด `yy_anchor_invalid` ํ›„๋ณด ๊ธฐ๋ก

๋ฐ”๋€ ํŒŒ์ผ
- `src/pykis/streaming/raw/market.py`
- `src/pykis/streaming/raw/adapter.py`
- `src/pykis/streaming/raw/parser.py`
- `src/pykis/streaming/router.py`
- `src/pykis/streaming/raw/__init__.py`
- `src/pykis/streaming/__init__.py`

ํ…Œ์ŠคํŠธ
- `python -m ruff check src/pykis/streaming`
- `python -m ruff format src/pykis/streaming`
- `python -m ty check src/pykis/streaming`

๋‹ค์Œ ์„ ํƒ
1) WebSocket ์‹ค์ œ ์—ฐ๊ฒฐ ๋ชจ๋“ˆ(์„ธ์…˜/๊ตฌ๋…/๋ณตํ˜ธํ™”)๊นŒ์ง€ ๊ฐ™์ด ๋ถ™์ด๊ธฐ
2) ์‹œ์žฅ ์ฝ”๋“œ ํ•ด์„ ๋กœ์ง ํ™•์žฅ(์˜ˆ: ์œ ๋Ÿฝ/๊ธฐํƒ€ ๊ฑฐ๋ž˜์†Œ ์ฝ”๋“œ ์ถ”๊ฐ€)
3) override JSONL์— batch flush / ํšŒ์ „(log rotate) ์ถ”๊ฐ€

์›ํ•˜๋Š” ๋ฐฉํ–ฅ ๋งํ•ด์ค˜.
- ํ•ด์™ธ ์‹ค์‹œ๊ฐ„ timezone์„ ์‹œ์žฅ ์ฝ”๋“œ๋กœ ๊ฒฐ์ •ํ•˜๋„๋ก ์ •๋ฆฌํ•˜๊ณ , ํŒŒ์‹ฑ ์ „์— override ์ ์šฉ๋˜๊ฒŒ ๊ตฌ์„ฑํ–ˆ์–ด์š”. (`src/pykis/streaming/raw/market.py`, `src/pykis/streaming/raw/adapter.py`, `src/pykis/streaming/raw/__init__.py`)
- Decimal/Datetime ์—์ง€ ์ผ€์ด์Šค๋ฅผ JSONL๋กœ ๋ˆ„์  ๊ธฐ๋กํ•˜๋„๋ก ๋กœ๊ทธ๋ฅผ ๋ฒ„ํผ๋ง/๋กœํ…Œ์ด์…˜ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ™•์žฅํ–ˆ์–ด์š”. (`src/pykis/streaming/raw/overrides.py`)
- YYMMDD๋Š” **์‹œ์žฅ timezone ๊ธฐ์ค€ ํ˜„์žฌ ์„ธ๊ธฐ**๋กœ๋งŒ ๊ฒฐ์ •ํ•˜๋„๋ก ๋‹จ์ˆœํ™”ํ–ˆ์–ด์š”. (`src/pykis/streaming/raw/parser.py`)
- WebSocket Layer 0 ์ „์†ก/์„ธ์…˜/๋ณตํ˜ธํ™” ํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ  API surface๋ฅผ ๋…ธ์ถœํ–ˆ์–ด์š”. (`src/pykis/streaming/transport.py`, `src/pykis/streaming/session.py`, `src/pykis/streaming/crypto.py`, `src/pykis/streaming/__init__.py`)
- ์‹คํ–‰ ํ”Œ๋žœ์„ 1~3 ์ž‘์—… ํฌํ•จํ•ด์„œ ์ƒ์„ธํžˆ ๋ณด๊ฐ•ํ•˜๊ณ  ์ง„ํ–‰ ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ ํ–ˆ์–ด์š”. (`.cursor/plans/pykis-layered-refactor_d66a42e7.plan.md`)

**business_date ์„ค๋ช…**
- `business_date`๋Š” **์‹ค์‹œ๊ฐ„ payload์— ๋‚ ์งœ๊ฐ€ ์—†๊ณ  ์‹œ๊ฐ„๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ**(timeโ€‘only) ๋ถ™์ด๋Š” **์‹œ์žฅ ๋กœ์ปฌ ๊ธฐ์ค€ ๊ฑฐ๋ž˜์ผ**์ž…๋‹ˆ๋‹ค.
- `StreamingParseContext`์—์„œ `business_date`๊ฐ€ ๋ช…์‹œ๋˜๋ฉด ๊ทธ ๊ฐ’์„ ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด `TimeProvider.now_utc()`๋ฅผ **์‹œ์žฅ timezone**์œผ๋กœ ๋ณ€ํ™˜ํ•ด `.date()`๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. (`src/pykis/streaming/raw/context.py`)
- ์‹ค์‹œ๊ฐ„ ์‘๋‹ต์€ ๋ณดํ†ต ๋‹น์ผ ์„ธ์…˜์ด๋ฏ€๋กœ **๋กœ์ปฌ(NTP ๋™๊ธฐํ™”) ๊ธฐ์ค€ ์‹œ๊ฐ**์„ ๊ธฐ๋ณธ์œผ๋กœ ์žก๊ณ , ์‹œ๊ฐ„์ด ์—ญํ–‰ํ•˜๋ฉด ๋งˆ์ง€๋ง‰ inferred ๋‚ ์งœ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ override ํ›„๋ณด๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. (`src/pykis/streaming/raw/parser.py`)

**3๋ฒˆ(WS transport) ์ƒ์„ธ**
- `WebSocketTransport`๊ฐ€ connect/send/messages๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. `ping_interval`๊ณผ `connect_timeout`์„ ๋…ธ์ถœํ•ด ๋„คํŠธ์›Œํฌ ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ•˜๊ณ , ๋ฉ”์‹œ์ง€๋Š” ๋ฌธ์ž์—ด๋กœ ์ •๊ทœํ™”ํ•ฉ๋‹ˆ๋‹ค. (`src/pykis/streaming/transport.py`)
- `StreamingSession`์€ ๊ตฌ๋…/ํ•ด์ง€ ๋ฉ”์‹œ์ง€ ์ „์†ก๊ณผ **control JSON** ์ˆ˜์‹ ์„ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, `body.output.key/iv`๊ฐ€ ์˜ค๋ฉด `StreamingKeyStore`์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. (`src/pykis/streaming/session.py`)
- payload ๋ณตํ˜ธํ™”๋Š” `AesCbcDecryptor`๊ฐ€ `cryptography` AESโ€‘CBC๋ฅผ ์‚ฌ์šฉํ•ด ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ํ‚ค/IV ๋ฏธ์„ค์ • ์‹œ ๋ช…ํ™•ํžˆ ์—๋Ÿฌ๋ฅผ ๋ƒ…๋‹ˆ๋‹ค. (`src/pykis/streaming/crypto.py`)
- ์ตœ์ข… ํ๋ฆ„: websocket ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  โ†’ control์ด๋ฉด ํ‚ค ๊ฐฑ์‹ , ๋ฐ์ดํ„ฐ๋ฉด `StreamingFrameHandler`๋กœ ์ „๋‹ฌ โ†’ L0 Decimal/UTC ํŒŒ์‹ฑ.

**ํ…Œ์ŠคํŠธ**
- `ruff format src/pykis/streaming src/pykis/streaming/raw`
- `ruff check src/pykis/streaming src/pykis/streaming/raw`
- `ty check src/pykis/streaming src/pykis/streaming/raw`

์›ํ•˜๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ด์–ด์„œ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์š”:
1) ํ•ด์™ธ ์‹œ์žฅ ์ฝ”๋“œ ๋งคํ•‘ ํ…Œ์ด๋ธ” ํ™•์žฅ/์ •์˜ ํ™•์ •(๊ฑฐ๋ž˜์†Œ ์ถ”๊ฐ€, ๋‚ด๋ถ€ ์ฝ”๋“œ ๊ทœ์น™ ํฌํ•จ)
2) SessionPool/SubscriptionRegistry ์—ฐ๊ฒฐํ•ด์„œ ์‹ค์ œ ๊ตฌ๋… ํ•ธ๋“ค๊นŒ์ง€ ์™„์„ฑ
3) codegen ์žฌ์‹คํ–‰(์ŠคํŽ™ ๋ณ€๊ฒฝ ๋ฐ˜์˜ ํ•„์š” ์‹œ)
๋ณ€๊ฒฝ ๋‚ด์šฉ
- ์„ธ์…˜ ๊ณต์œ /์ œํ•œ: `SessionPool`๋กœ `(app_key, domain)`๋‹น 1 ์„ธ์…˜๋งŒ ์œ ์ง€ํ•˜๊ณ  ref-count๋กœ ๊ณต์œ . `src/pykis/streaming/pool.py`
- ๊ตฌ๋… ์ œํ•œ: `SubscriptionRegistry`์— 41๊ฑด cap ์ ์šฉ. `src/pykis/streaming/subscriptions.py`, `src/pykis/streaming/constants.py`
- ๋ฐฑํ”„๋ ˆ์…” ํ: `StreamQueue` + drop policy(๊ธฐ๋ณธ drop_oldest). `src/pykis/streaming/queue.py`
- ๊ณต๊ฐœ API ์ •๋ฆฌ: ์œ„ ๊ตฌ์„ฑ์š”์†Œ๋ฅผ `pykis.streaming`์—์„œ ๋…ธ์ถœ. `src/pykis/streaming/__init__.py`
- ๋ฌธ์„œ ๊ธฐ์ค€ ์„ค๊ณ„ ๋ฐ˜์˜: REST/WS ์œ ๋Ÿ‰ ์ •์ฑ…์„ ํ”Œ๋žœ์— ๋ช…์‹œ. `.cursor/plans/pykis-layered-refactor_d66a42e7.plan.md`

FAQ ๊ธฐ์ค€ ์„ค๊ณ„ ์š”์•ฝ (2025โ€‘04โ€‘28 ๊ธฐ์ค€ ๋ฌธ์„œ)
- REST: ๊ณ„์ขŒ(์•ฑํ‚ค) ๋‹จ์œ„
  - real: 20 req/sec โ†’ ๊ธฐ๋ณธ ์ง€์—ฐ 0.05s
  - demo: 2 req/sec โ†’ ๊ธฐ๋ณธ ์ง€์—ฐ 0.5s
- WebSocket: ๊ณ„์ขŒ(์•ฑํ‚ค) ๋‹จ์œ„ 1 ์„ธ์…˜, ์‹ค์‹œ๊ฐ„ ๊ตฌ๋… ์ดํ•ฉ 41๊ฑด

์ฝ”๋“œ์  
- `PYTHONPATH=src python - <<'PY' โ€ฆ run_pipeline()`๋กœ streaming codegen ์žฌ์‹คํ–‰ ์™„๋ฃŒ (๋ณ€๊ฒฝ ์—†์Œ).

ํ…Œ์ŠคํŠธ
- `ruff format src/pykis/streaming src/pykis/streaming/raw`
- `ruff check src/pykis/streaming src/pykis/streaming/raw`
- `ty check src/pykis/streaming src/pykis/streaming/raw`

๋‹ค์Œ ๋‹จ๊ณ„ ์ œ์•ˆ
1) `SessionPool` + `SubscriptionRegistry`๋ฅผ `StreamingSession.subscribe/unsubscribe`์— ์—ฐ๊ฒฐํ•ด ์‹ค์ œ ๊ตฌ๋… ์นด์šดํŠธ ๊ฐ•์ œ
2) REST `KisRawClient` ๊ธฐ๋ณธ rate_limit์„ domain ๊ธฐ๋ฐ˜์œผ๋กœ ์ ์šฉํ• ์ง€ ๊ฒฐ์ •(์‹ค์ „ 0.05s / ๋ชจ์˜ 0.5s)
โ€ฆ์ฑ…๋„ ์„ค๊ณ„์— ๋ฐ˜์˜ํ–ˆ๊ณ , ๊ตฌํ˜„์€ ์•„๋ž˜์™€ ๊ฐ™์•„์š”.

๋ณ€๊ฒฝ ์š”์•ฝ
- ๊ตฌ๋… ์ œํ•œ ๊ฐ•์ œ: `SessionEntry.subscribe/unsubscribe`๊ฐ€ `SubscriptionRegistry`๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•ด 41๊ฑด cap์„ ๊ฐ•์ œํ•˜๊ณ , ์‹คํŒจ ์‹œ ๋กค๋ฐฑํ•ฉ๋‹ˆ๋‹ค. `src/pykis/streaming/pool.py` `src/pykis/streaming/subscriptions.py`
- ๊ณ ์† rateโ€‘limit: perโ€‘second/perโ€‘minute ํ† ํฐ ๋ฒ„ํ‚ท ๊ธฐ๋ฐ˜ limiter + 429 ์‹œ `Retryโ€‘After` ๋ฐ˜์˜ ๋Œ€๊ธฐ ํ›„ ์žฌ์‹œ๋„. `src/pykis/api/rate_limit.py` `src/pykis/api/_async/client.py` `src/pykis/api/_sync/client.py`
- ํ”Œ๋žœ ๊ฐฑ์‹ : FAQ ๊ธฐ๋ฐ˜ rateโ€‘limit ์ •์ฑ… ๋ฐ ์ ์šฉ ์„ค๊ณ„ ๋ฐ˜์˜. `.cursor/plans/pykis-layered-refactor_d66a42e7.plan.md`

๊ตฌํ˜„ ์ƒ์„ธ (์š”๊ตฌ์‚ฌํ•ญ ๋Œ€์‘)
- **์ตœ๋Œ€ ์œ ๋Ÿ‰ ๊ทผ์ ‘ ์ „์†ก**: ์š”์ฒญ๋งˆ๋‹ค `RateLimiter.acquire()`๋กœ ํ† ํฐ์„ ์ •ํ™•ํžˆ ๊ธฐ๋‹ค๋ ค ์ตœ๋Œ€ ํ•œ๊ณ„๊นŒ์ง€ ๋ฐ€์–ด๋ถ™์ด๋„๋ก ๊ตฌ์„ฑ.
- **429 ์ฒ˜๋ฆฌ**: `Retryโ€‘After`๋ฅผ ํŒŒ์‹ฑํ•ด cooldown์— ๋ฐ˜์˜ โ†’ ํ•ด์ œ๊นŒ์ง€ ๋Œ€๊ธฐ ํ›„ ์ž๋™ ์žฌ์‹œ๋„. `rate_limit_max_retries=None`์ด๋ฉด ๋ฌดํ•œ ๋Œ€๊ธฐ.
- **๋„๋ฉ”์ธ ๊ธฐ๋ณธ๊ฐ’**: real=20 req/sec, demo=2 req/sec (๋ฌธ์„œ ๊ธฐ์ค€). ๊ธฐ์กด `rate_limit`(์ดˆ ๋‹จ์œ„ ์ง€์—ฐ)๋„ ์œ ์ง€๋˜๋ฉฐ, ์ง€์ • ์‹œ `1/rate_limit`๋กœ ํ™˜์‚ฐ.

์‚ฌ์šฉ ์˜ต์…˜ (ํ•„์š” ์‹œ)
- `rate_limit_per_second`, `rate_limit_per_minute`: ํ•œ๊ณ„ ์ง์ ‘ ์ง€์ •
- `rate_limit_max_retries`: 429 ์žฌ์‹œ๋„ ์ƒํ•œ (None=๋ฌดํ•œ)
- `rate_limit_min_backoff`: Retryโ€‘After ์—†์„ ๋•Œ ๊ธฐ๋ณธ ๋Œ€๊ธฐ (๊ธฐ๋ณธ 0.5s)

ํ…Œ์ŠคํŠธ
- `ruff format src/pykis/api src/pykis/streaming`
- `ruff check src/pykis/api src/pykis/streaming`
- `ty check src/pykis/api src/pykis/streaming` โ†’ `httpx_retries` ๋ฏธ์„ค์น˜๋กœ ์‹คํŒจ (ํ™˜๊ฒฝ ๋ฌธ์ œ)

์›ํ•˜๋ฉด ๋‹ค์Œ์œผ๋กœ 1) `SessionPool`์„ ์‹ค์ œ ์ŠคํŠธ๋ฆฌ๋ฐ ์†Œ๋น„์ž ๋ ˆ์ด์–ด์— ์—ฐ๊ฒฐ, 2) REST ์š”์ฒญ ํ๋ฆ„์— perโ€‘minute ์ œํ•œ ์ ์šฉ ๊ธฐ๋ณธ๊ฐ’๊นŒ์ง€ ์ด์–ด์„œ ์ง„ํ–‰ํ• ๊ฒŒ์š”.
์ง„ํ–‰ ๋‚ด์šฉ
- `cryptography` ์˜์กด์„ฑ ์ถ”๊ฐ€: streaming AES ๋ณตํ˜ธํ™” ํƒ€์ž… ํ•ด์†Œ. `pyproject.toml`, `uv.lock`
- `uv sync`๋กœ .venv ๊ฐฑ์‹ 
- ruff ์ „์ฒด ์‹คํ–‰
- ty ๋ถ„ํ•  ์‹คํ–‰ + ์ „์ฒด ์‹คํ–‰ (system `ty` + `.venv/bin/python` ์กฐํ•ฉ)

๋ถ„ํ•  `ty` ๊ฒฐ๊ณผ
- `ty check src/pykis/api --python .venv/bin/python` โœ…
- `ty check src/pykis/store --python .venv/bin/python` โœ…
- `ty check src/pykis/raw --python .venv/bin/python` โœ…
- `ty check src/pykis/streaming --python .venv/bin/python` โœ… (cryptography ์„ค์น˜ ํ›„)
- `ty check src/codegen --python .venv/bin/python` โœ…
- `ty check src/docsgen --python .venv/bin/python` โœ…
- `ty check src --python .venv/bin/python` โœ…

์˜ค๋ž˜ ๊ฑธ๋ฆฐ ์›์ธ
- `.venv/bin/ty` (0.0.12)๋Š” `--help`์กฐ์ฐจ ํƒ€์ž„์•„์›ƒ๋˜๋Š” ํ˜„์ƒ์ด ์žˆ์—ˆ๊ณ , ์ด ๋ฐ”์ด๋„ˆ๋ฆฌ๊ฐ€ ์ „์ฒด `ty check src`๋ฅผ 180s ์ด์ƒ ์ง€์—ฐ์‹œํ‚ค๋Š” ์›์ธ์ด์—ˆ์Œ.
- system `ty`(0.0.1-alpha)๋กœ ์‹คํ–‰ํ•˜๋ฉด ์ฆ‰์‹œ ์™„๋ฃŒ๋จ.
- ์ถ”๊ฐ€๋กœ `cryptography` ๋ฏธ์„ค์น˜ ์ƒํƒœ์—์„œ๋Š” `streaming/crypto.py`์—์„œ unresolved-import ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒ.

์‹คํ–‰ ๋กœ๊ทธ ์š”์•ฝ
- `ruff format src` โœ…
- `ruff check src` โœ…
- `ty check src --python .venv/bin/python --no-progress` โœ…

๋ณ€๊ฒฝ ํŒŒ์ผ
- `pyproject.toml`
- `uv.lock`

์›ํ•˜๋ฉด ๋‹ค์Œ ์ค‘ ์ง„ํ–‰ํ• ๊ฒŒ:
1) `.venv/bin/ty` ํƒ€์ž„์•„์›ƒ ์›์ธ ์ถ”๊ฐ€ ๋ถ„์„(ํ”„๋กœํŒŒ์ผ๋ง/๋กœ๊ทธ ์˜ต์…˜)
2) `ty` ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ(๋ถ„ํ•  ์‹คํ–‰ ๊ณ ์ •) ์ถ”๊ฐ€
์™„๋ฃŒ ์‚ฌํ•ญ
- `cryptography==46.0.3` ๋ฐ˜์˜: `pyproject.toml`
- `uv sync` ๊ฐฑ์‹ : `uv.lock`
- `ruff format src` / `ruff check src` โœ…
- `ty check src --python .venv/bin/python --no-progress` โœ…

P4 ๋‚จ์€ ์ž‘์—… ๋ฆฌ์ŠคํŠธ (plan ๊ธฐ์ค€, status=pending)
- `p4-event-router` โ€” EventRouter keyed by (tr_id, tr_key)
- `p4-streaming-normalize` โ€” Streaming Layer 1 ์ •๊ทœํ™”
- `p4-streaming-system` โ€” Streaming Layer 2 ์‹œ์Šคํ…œ ์ด๋ฒคํŠธ
- `p4-reconnect-flow` โ€” reconnect/resubscribe deterministic replay
- `p4-unasync-streaming` โ€” unasync๋กœ sync ์ŠคํŠธ๋ฆฌ๋ฐ facade

๋ณ€๊ฒฝ ํŒŒ์ผ
- `pyproject.toml`
- `uv.lock`

๋‹ค์Œ์œผ๋กœ ๋ฌด์—‡๋ถ€ํ„ฐ ์ง„ํ–‰ํ• ๊นŒ?
โ€ฆ ๋ณด์™„ํ–ˆ์–ด. ์ง€๊ธˆ์€ `_async`๋ฅผ ์†Œ์Šค๋กœ ๋‘๊ณ , `scripts/build_unasync.py`๊ฐ€ sync๋ฅผ ์ œ๋Œ€๋กœ ์ƒ์„ฑํ•˜๋„๋ก ๋ณด๊ฐ•๋œ ์ƒํƒœ์•ผ.

- `src/pykis/streaming/_async/*`๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ ์†Œ์Šค ์ด๋™ + `src/pykis/streaming/__init__.py`์—์„œ async/sync export ์ •๋ฆฌ
- `src/pykis/streaming/normalize`์™€ `src/pykis/streaming/system` ์ถ”๊ฐ€(๊ตญ๋‚ด/ํ•ด์™ธ Trade/OrderBook ์ •๊ทœํ™” + ์‹œ์Šคํ…œ ๊ณ„์‚ฐ)
- `StreamingEventRouter`/`StreamQueue`/`SessionPool` ๋™์ž‘ ๋ณด๊ฐ• ๋ฐ reconnect/resubscribe ๋ฃจํ”„ ์—ฐ๊ฒฐ
- `src/pykis/streaming/_async/transport.py`์—์„œ `open_timeout` ๊ธฐ๋ฐ˜ ์—ฐ๊ฒฐ๋กœ ์ „ํ™˜
- `scripts/build_unasync.py`์—์„œ ์ŠคํŠธ๋ฆฌ๋ฐ sync ์ƒ์„ฑ ํ›„์ฒ˜๋ฆฌ ๊ฐ•ํ™”(threads/queue/time, websockets.sync, connect/close/lock ๋ณ€ํ™˜)

**๋ณ€๊ฒฝํ•œ ํŒŒ์ผ๋“ค**
- `scripts/build_unasync.py`
- `src/pykis/streaming/__init__.py`
- `src/pykis/streaming/_async/*`
- `src/pykis/streaming/_sync/*` (์ƒ์„ฑ๋จ)
- `src/pykis/streaming/normalize/*`
- `src/pykis/streaming/system/*`

**ํ…Œ์ŠคํŠธ/๊ฒ€์ฆ**
- `ruff`/`ty` ์‹คํ–‰ ์‹œ๋„ํ–ˆ์ง€๋งŒ, ๋‘˜ ๋‹ค **์‹คํ–‰ ์ž์ฒด๊ฐ€ ๋ฉˆ์ถค** (์‹ฌ์ง€์–ด `--version`๋„ 5~20์ดˆ ๋‚ด ์‘๋‹ต ์—†์Œ).
- ์›์ธ ์ถ”์ •: macOS ๋ณด์•ˆ/๊ถŒํ•œ ์ด์Šˆ ๋˜๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ ์†์ƒ/์‹คํ–‰ ์ฐจ๋‹จ (๋‘ ๋ฐ”์ด๋„ˆ๋ฆฌ์— `com.apple.provenance` xattr๊ฐ€ ์กด์žฌ).

**๋‹ค์Œ ์ œ์•ˆ (ํ•„์š”์‹œ)**
1) `xattr -d com.apple.provenance .venv/bin/ruff .venv/bin/ty` ํ›„ ์žฌ์‹œ๋„
2) `uv pip install -U ruff ty`๋กœ ์žฌ์„ค์น˜
3) `ruff format src` / `ruff check src` / `ty check src --python .venv/bin/python --no-progress` ์žฌ์‹คํ–‰

์›ํ•˜๋ฉด ์œ„ 1~3 ์ค‘ ์‹คํ–‰ํ•  ๊ฑธ ์•Œ๋ ค์ค˜.
โ€ฆ๊ตฌํ•œ ๋’ค, `ruff`/`ty` ๋ชจ๋‘ ํ†ต๊ณผํ–ˆ์–ด.

๋ณ€๊ฒฝ ๋‚ด์šฉ ์š”์•ฝ
- ์ŠคํŠธ๋ฆฌ๋ฐ ์ •๊ทœํ™”์—์„œ ๋ฌธ์ž์—ด ์ˆซ์ž ํ•„๋“œ๋„ `Decimal`๋กœ ์•ˆ์ • ๋ณ€ํ™˜ํ•˜๋„๋ก ๋ณด๊ฐ•ํ•˜๊ณ , ์กด์žฌํ•˜์ง€ ์•Š๋Š” `H0UPASP0` ์ œ๊ฑฐ. (`src/pykis/streaming/normalize/mapper.py`)
- `SessionPool.acquire`์˜ transformer ํƒ€์ž…์„ `EventTransformer`๋กœ ์ •๋ฆฌ. (`src/pykis/streaming/_async/pool.py`)
- codegen ํ•„๋“œ alias ํ‚ค ํŒŒ์‹ฑ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ฐ•. (`src/codegen/streaming/steps/io.py`)
- unasync ํ›„์ฒ˜๋ฆฌ์—์„œ sync ์ฝ”๋“œ ํƒ€์ž…/์ž„ํฌํŠธ ์ •ํ•ฉ์„ฑ ๋ณด์™„. (`scripts/build_unasync.py`)

๊ฒ€์ฆ
- `ruff format src`
- `ruff check src`
- `ty check src --python .venv/bin/python --no-progress`

์ถ”๊ฐ€๋กœ ์›ํ•˜๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์•Œ๋ ค์ค˜.
ํ˜„์žฌ api codegen๊ณผ ๋‹ค๋ฅด๊ฒŒ store์˜ codegen์—์„œ๋Š”

    dow\_30\_yn: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 1},
    )
    ("๋‹ค์šฐ30 ํŽธ์ž…์ข…๋ชฉ์—ฌ๋ถ€ 0:๋ฏธํŽธ์ž… 1:ํŽธ์ž…")
    nasdaq\_100\_yn: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 1},
    )
    ("๋‚˜์Šค๋‹ฅ100 ํŽธ์ž…์ข…๋ชฉ์—ฌ๋ถ€ 0:๋ฏธํŽธ์ž… 1:ํŽธ์ž…")
    snp\_500\_yn: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 1},
    )
    ("S&P 500 ํŽธ์ž…์ข…๋ชฉ์—ฌ๋ถ€ 0:๋ฏธํŽธ์ž… 1:ํŽธ์ž…")

์ด์ฒ˜๋Ÿผ boolean ํ˜•ํƒœ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ํŒŒ์‹ฑํ•˜์ง€ ๋ชปํ•˜๊ณ  ์žˆ์–ด, ์ด๊ฑฐ ์›์ธํŒŒ์•… ํ›„
ํ•ด๊ฒฐํ•ด์ค˜, ๋˜ํ•œ api์™€ ๊ธฐ๋Šฅ์  ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก core๋กœ ๋ฌถ๋Š” ๊ฒƒ๋„ ๊ณ ๋ คํ•ด์ค˜
class ST\_FO\_IDX\_CODE(StoreModel):
    info\_type: str = Field(
        json\_schema\_extra={"length": 1},
    )
    (
        "1:์ง€์ˆ˜์„ ๋ฌผ 2:์ง€์ˆ˜SP 3:์Šคํƒ€์„ ๋ฌผ 4:์Šคํƒ€SP "
        "5:์ง€์ˆ˜์ฝœ์˜ต์…˜ 6:์ง€์ˆ˜ํ’‹์˜ต์…˜ "
        "7:๋ณ€๋™์„ฑ์„ ๋ฌผ 8:๋ณ€๋™์„ฑSP "
        "9:์„นํ„ฐ์„ ๋ฌผ A:์„นํ„ฐSP "
        "B:๋ฏธ๋‹ˆ์„ ๋ฌผ C:๋ฏธ๋‹ˆSP "
        "D:๋ฏธ๋‹ˆ์ฝœ์˜ต์…˜ E:๋ฏธ๋‹ˆํ’‹์˜ต์…˜ "
        "J:์ฝ”์Šค๋‹ฅ150์ฝœ์˜ต์…˜ K:์ฝ”์Šค๋‹ฅ150ํ’‹์˜ต์…˜ "
        "L:์œ„ํด๋ฆฌ์ฝœ์˜ต์…˜ M:์œ„ํด๋ฆฌํ’‹์˜ต์…˜"
    )

    mmsc\_cls\_code: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 1},
    )
    ("์›”๋ฌผ๊ตฌ๋ถ„์ฝ”๋“œ (0:์—ฐ๊ฒฐ์„ ๋ฌผ, 1:์ตœ๊ทผ์›”๋ฌผ 2:์ฐจ๊ทผ์›”๋ฌผ 3:์ฐจ์ฐจ๊ทผ์›”๋ฌผ 4:์ฐจ์ฐจ์ฐจ๊ทผ์›”๋ฌผ")

    atm\_cls\_code: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 1},
    )
    ("ATM๊ตฌ๋ถ„(1:ATM,2:ITM,3:OTM)")

    type: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 2},
    )
    ("A0:์žฅ๋‚ด์†Œ๋งค์ฑ„๊ถŒ, F9:(์ฃผ์‹๊ด€๋ จ์‚ฌ์ฑ„, ์†Œ์•ก์ฑ„๊ถŒ), C0:๊ตญ๊ณ ์ฑ„๊ถŒ")

        bond\_cls\_code: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 2},
    )
    (
        "A0: GA:๊ตญ๊ณ ์ฑ„ MA:ํ†ต์•ˆ์ฑ„ BA:๊ธˆ์œต์ฑ„ SA:๋น„๊ธˆ์œตํŠน์ˆ˜์ฑ„ CA:ํšŒ์‚ฌ์ฑ„ "
        "F9: 03:์žฅ๋‚ด์†Œ์•ก์ฑ„๊ถŒ 02:๊ธฐํƒ€์ฑ„๊ถŒ 04:์žฅ๋‚ด์ฃผ์‹๊ด€๋ จ์‚ฌ์ฑ„ MM:๋งค๋งค์ข…๋ฅ˜๋ณ„ "
        "C0: GA:๊ตญ๊ณ ์ฑ„ MA:ํ†ต์•ˆ์ฑ„ BA:๊ธˆ์œต์ฑ„ SA:๋น„๊ธˆ์œตํŠน์ˆ˜์ฑ„ MB:์ง€๋ฐฉ์ฑ„ 99:๊ธฐํƒ€"
    )

        bond\_int\_cls\_code: str | None = Field(
        default=None,
        json\_schema\_extra={"length": 2},
    )
    (
        "F9/C0 : 01:ํ• ์ธ 02:๋ณต๋ฆฌ 03:์ดํ‘œ 04:๊ธˆ๋ฆฌ์—ฐ๋™ 05:๋ถ„ํ• ์ƒํ™˜(๊ฑฐ๋ฆฌ๋ณต๋ฆฌ) "
        "06:๋ถ„ํ• (๊ฑฐ์น˜๋‹จ๋ฆฌ) 07:๋‹จ๋ฆฌ 08:FRN 09:๋ณต5๋‹จ2 "
        "A0 : ๊ณ ์ •๊ธˆ๋ฆฌํ˜• 11:ํ• ์ธ์ฑ„ 12:๋ณต๋ฆฌ์ฑ„ 13:์ดํ‘œ์ฑ„ 14:๋‹จ๋ฆฌ์ฑ„ 15:๋ณต5๋‹จ2 19:๊ธฐํƒ€ "
        "A0 : ๋ณ€๋™๊ธˆ๋ฆฌํ˜• 21:์ดํ‘œ์ฑ„ 22:๋ณต๋ฆฌ์ฑ„ 23:๋‹จ๋ฆฌ์ฑ„ 29:๊ธฐํƒ€"
    )

    โ€‹ํ˜„์žฌ store์—์„œ enum ํŒŒ์‹ฑ์ด ์œ„์— ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ์ผ€์ด์Šค๊ฐ€ ๋‹ค์ˆ˜ ์กด์žฌํ•ด, codegen์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  enum ์ผ€์ด์Šค๋ฅผ ๋ฆฌ์ŠคํŠธ์—…ํ•˜๊ณ , ๋ชจ๋“  ์œ ํ˜•์„ ์ •์ƒ ํŒŒ์‹ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐ•ํ™”ํ•ด์ค˜
โ€ฆatting allows 150โ€‘char lines and avoids magic trailing-comma expansion, keeping those outputs from being lineโ€‘wrapped by the root settings.

- Created `src/pykis/api/raw/ruff.toml`, `src/pykis/store/raw/ruff.toml`, `src/pykis/streaming/raw/ruff.toml` with the requested overrides (line length 150, py313 target, skip magic trailing comma, E/F/I lint select).

**Notes on formatting**
- I ran `uv run ruff format src/pykis/api/raw src/pykis/store/raw src/pykis/streaming/raw` to satisfy the postโ€‘task requirement; it reformatted 335 files in those directories (plus `src/pykis/api/_sync/client.py` due to formatter reach). Ruff also warned about `isort.split-on-trailing-comma` being incompatible with `format.skip-magic-trailing-comma=true`.

**Type check**
- `uv run ty check` failed with `invalid-context-manager` at `src/pykis/api/_sync/client.py:339` and `src/pykis/api/_sync/client.py:360` (sync generated code). Per your rules I didnโ€™t edit sync code; this likely needs a fix in async source or unasync/generator.

If you want, I can:
1) Revert the formatter changes and keep only the new `ruff.toml` files.
2) Fix the ty errors by updating the async source and regenerating sync code.
โ€ฆted[...]` and collapse wrapped description lines into a single string, while autoโ€‘importing `Annotated` when any field has a description. I also fixed a ruff import-order issue in the unit test.

- `src/codegen/templates/store/struct_module.py.jinja`: wrap types in `Annotated[..., "desc"]` when `field.desc` is present and remove the old docstring tuple block.
- `src/codegen/store/steps/generate.py`: store a single-string `desc` (joined wrap lines) and add `Annotated` to `typing_imports` when needed.
- `tests/unit/test_token_refresh.py`: reorder thirdโ€‘party imports to satisfy ruff.

Tests:
- `uv run ruff check .` (pass)
- `uv run ty check` (fail: `invalid-context-manager` in `src/pykis/api/_sync/client.py:339` and `src/pykis/api/_sync/client.py:360`)

If you want, I can also regenerate the store structs (e.g. `uv run python -m codegen.store.cli`).
โ€ฆarenthesized, implicitly concatenated literals (using displayโ€‘widthโ€‘aware wrapping) and wired it into the Jinja pipeline so generated Annotated/enum descriptions avoid E501.

**Changes**
- `src/codegen/core/text.py`: added `format_string_literal` plus a helper to split long literals by repr length and wrap with indentation/parentheses.
- `src/codegen/core/render.py`: registered the `string_literal` Jinja filter.
- Templates updated to use the new filter for description literals: `src/codegen/templates/store/struct_module.py.jinja`, `src/codegen/templates/store/enums_module.py.jinja`, `src/codegen/templates/api/raw_module.py.jinja`.

**Tests**
- ` .venv/bin/ruff check` โœ…
- `.venv/bin/ty check` โŒ (existing issues in generated sync code): `src/pykis/api/_sync/client.py:339`, `src/pykis/api/_sync/client.py:360` (Lock used with `with`).

Next steps you may want:
1) Regenerate codegen outputs and reโ€‘run `ruff check src/pykis/store/raw/structs/` and `ruff check src/pykis/api/raw/`.
2) Decide how you want to address the `ty` errors in sync code (itโ€™s generated, so the fix should come from the async source/unasync).
Soju06 added 17 commits January 23, 2026 17:35
โ€ฆsync API sync generation to convert `asyncio.Lock` to `threading.Lock`, regenerated sync client, and re-ran API codegen so the `Unique key(0)` descriptions no longer produce enums. The regenerated raw outputs now omit `OrdSvrDvsnCdEnum` in `src/pykis/api/raw/์žฅ๋‚ด์ฑ„๊ถŒ/์ฃผ๋ฌธ_๊ณ„์ขŒ/buy.py`, while `src/pykis/api/raw/์žฅ๋‚ด์ฑ„๊ถŒ/์ฃผ๋ฌธ_๊ณ„์ขŒ/order_rvsecncl.py` still includes `RvseCnclDvsnCdEnum` with `01/02`, and `src/pykis/api/raw/registry.py` was refreshed.

**Changes**
- `src/codegen/core/enums.py`: added a format-consistency check to drop mixed purely alpha/purely numeric codes before returning enums.
- `scripts/build_unasync.py`: added API sync postprocessing to rewrite `asyncio.Lock` to `threading.Lock` and ensured threading import; regenerated `src/pykis/api/_sync/client.py` via `scripts/build_unasync.py`.
- `src/pykis/api/raw/**`: regenerated API raw models and registry via the codegen pipeline, including removal of `OrdSvrDvsnCdEnum` from the bond buy request.

**Verification**
- `.venv/bin/python -c "from codegen.api.pipeline import run_pipeline; run_pipeline()"`
- `.venv/bin/ruff format .` (warning: `isort.split-on-trailing-comma` vs `format.skip-magic-trailing-comma`)
- `.venv/bin/ty check`

If you want me to scope codegen to specific sections to reduce the raw regen diff, I can re-run with `TARGET_SECTIONS`.
โ€ฆ`๋งŒ ๋‚จ๋„๋ก ๋ฐ”๊ฟจ์Šต๋‹ˆ๋‹ค. ํ•„๋“œ ์„ค๋ช…์€ ์ด๋ฏธ ์กด์žฌํ•˜๋˜ `RequestDict` ํด๋ž˜์Šค docstring์˜ โ€œRequest fieldsโ€์—๋งŒ ์œ ์ง€๋˜๋„๋ก ํ•ด์„œ โ€œdocstring ๋ฐฉ์‹โ€์œผ๋กœ ์ „ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ๋˜์ง€ ์•Š๋˜ `_literal_pieces`๋„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ์€ `src/codegen/api/steps/generate.py`์— ์žˆ์Šต๋‹ˆ๋‹ค.

Tests run:
- `\.venv/bin/ruff format src/codegen/api/steps/generate.py`
- `\.venv/bin/ruff check src/codegen/api/steps/generate.py`
- `\.venv/bin/ty check src/codegen/api/steps/generate.py`

๋‹ค์Œ ๋‹จ๊ณ„๊ฐ€ ํ•„์š”ํ•˜๋ฉด ์•Œ๋ ค์ค˜:
1) codegen ํŒŒ์ดํ”„๋ผ์ธ์„ ๋‹ค์‹œ ๋Œ๋ ค์„œ ์ƒ์„ฑ ํŒŒ์ผ๋“ค์„ ๊ฐฑ์‹ 
2) ruff/ty๋ฅผ ์ „์ฒด ๊ฒฝ๋กœ์— ๋Œ€ํ•ด ์‹คํ–‰
โ€ฆlds and extended streaming codegen to carry enum metadata from the spec and emit `Field(json_schema_extra=...)` when enums are present. This mirrors the store struct enum behavior while keeping existing `blank_allowed` formatting intact.

Details
- `src/codegen/api/steps/generate.py` merges enum/enum_desc (when present) with `blank_allowed` into a single `json_schema_extra`.
- `src/codegen/streaming/models.py` adds `enum`/`enum_desc` to `FieldSpec`.
- `src/codegen/streaming/steps/io.py` parses enum metadata and propagates it to alias specs.
- `src/codegen/streaming/steps/generate.py` imports `Field` when needed and emits enum `json_schema_extra` on streaming models.

Checks
- `./.venv/bin/ruff format src/codegen`
- `./.venv/bin/ruff check src/codegen`
- `./.venv/bin/ty check src/codegen`

Next steps (if you want generated outputs updated)
1) Run `python -m codegen.cli` and select 2 (API) and 4 (Streaming).
- `ruff format .` ์‹คํ–‰๋จ (warning: `isort.split-on-trailing-comma` vs `format.skip-magic-trailing-comma` ์ถฉ๋Œ)
- `ruff check .` ์‹คํŒจ: `I001` import ์ •๋ ฌ ๋ฌธ์ œ โ€” `src/pykis/api/_sync/client.py:1`
- `ty check .` ํ†ต๊ณผ

`_sync`๋Š” ์ƒ์„ฑ๋ฌผ์ด๋ผ ์ˆ˜๋™ ์ˆ˜์ •์€ ํ•˜์ง€ ์•Š์•˜๊ณ , `ruff format` ๋•Œ๋ฌธ์— ์ƒ์„ฑ๋œ raw ๋ชจ๋“ˆ/registry ๋“ฑ ๋งŽ์€ ํŒŒ์ผ์ด ๋ณ€๊ฒฝ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค(269 files reformatted).

๋‹ค์Œ ์ค‘ ๋ฌด์—‡์„ ํ• ๊นŒ์š”?
1) unasync ์žฌ์ƒ์„ฑ์œผ๋กœ `_sync`๋ฅผ ๋‹ค์‹œ ๋งŒ๋“ค๊ณ  `ruff check` ์žฌ์‹œ๋„
2) `ruff` ์„ค์ •์—์„œ `src/pykis/api/_sync` (๋˜๋Š” I001) ์ œ์™ธ ์ฒ˜๋ฆฌ
โ€ฆe๋กœ ๋ณ€๊ฒฝํ–ˆ๊ณ , API raw ๋ชจ๋ธ ํ•„๋“œ ์„ค๋ช… ์ถœ๋ ฅ๋„ ๋™์ผํ•œ formatter๋ฅผ ์“ฐ๋„๋ก ๋ฐ”๊ฟจ์Šต๋‹ˆ๋‹ค. ์ƒ์„ฑ ๊ทœ์น™ ๋ณ€๊ฒฝ์ด๋ผ `src/pykis/...` ๊ฒฐ๊ณผ๋ฌผ์€ ์†๋Œ€์ง€ ์•Š์•˜๊ณ , ํ•„์š” ์‹œ codegen ์žฌ์‹คํ–‰์œผ๋กœ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.

- `src/codegen/core/text.py`: `format_string_literal`๊ฐ€ `"""..."""` ํ˜•์‹์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜๊ณ , ์ค„๋ฐ”๊ฟˆ ์—†์ด ๊ธฐ์กด ๋ฌธ์ž์—ด ๊ฐ’ ์œ ์ง€(escape ํฌํ•จ)ํ•˜๋„๋ก ์ฒ˜๋ฆฌ.
- `src/codegen/templates/api/raw_module.py.jinja`: BaseModel ํ•„๋“œ ์„ค๋ช…์„ `string_literal`๋กœ ์ถœ๋ ฅํ•˜๋„๋ก ๋ณ€๊ฒฝ(๊ธฐ์กด `("docstring")` ํŒจํ„ด ์ œ๊ฑฐ).
- `src/codegen/api/steps/generate.py`: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” `desc_lines` ์ œ๊ฑฐ.

ํ…Œ์ŠคํŠธ/๊ฒ€์‚ฌ
- `ruff format src/codegen`
- `ruff check src/codegen`
- `ty check src/codegen`

๋‹ค์Œ ๋‹จ๊ณ„(ํ•„์š” ์‹œ)
1) store/api/streaming ๊ฒฐ๊ณผ๋ฌผ์„ ์‹ค์ œ ์ฝ”๋“œ์— ๋ฐ˜์˜ํ•˜๋ ค๋ฉด codegen ํŒŒ์ดํ”„๋ผ์ธ์„ ๋‹ค์‹œ ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”.
2) streaming ๋ชจ๋ธ์—๋„ ํ•„๋“œ ์„ค๋ช…(docstring)์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, spec์— ์„ค๋ช… ํ•„๋“œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ ํ›„ ์ƒ์„ฑ ๋กœ์ง์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
โ€ฆocstring ๊ตฌ์กฐ ๊ธฐ์ค€์œผ๋กœ streaming ๊ตฌ์ถ• ๊ณ„ํš์„ ์—…๋ฐ์ดํŠธํ–ˆ์–ด. ์ถ”๊ฐ€๋กœ ruff I001์„ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  unasync ํ›„์ฒ˜๋ฆฌ์—์„œ import ์‚ฝ์ž… ์ˆœ์„œ๋ฅผ ์ •๋ฆฌํ•˜๊ณ  sync ์ฝ”๋“œ๋ฅผ ์žฌ์ƒ์„ฑํ–ˆ์–ด.

**Plan**
- AS-IS ํ™•์ธ: `docs/kis/**/์‹ค์‹œ๊ฐ„์‹œ์„ธ/*.yaml`์— `response.body` ํ•„๋“œ(`code/name/description`)๊ฐ€ ์žˆ๊ณ , streaming spec์€ `tmp/docs/reports/streaming_fieldspec_draft.yaml`์— `field_specs`(type/source/rule)๋งŒ ์žˆ๋Š” ์ƒํƒœ. ์ƒ์„ฑ๊ธฐ๋Š” ์ด๋ฏธ `format_annotated_type`๋กœ desc๋ฅผ `Annotated[type, """desc"""]`๋กœ ์ถœ๋ ฅ ๊ฐ€๋Šฅ.
- ๋ฌธ์„œ ์ถ”์ถœ: YAML ํŒŒ์ผ๋ช…(stem)์„ TR ID๋กœ ๋งคํ•‘ํ•˜๊ณ  `response.body`์˜ `code โ†’ description` ํ…Œ์ด๋ธ” ์ƒ์„ฑ. API codegen๊ณผ ๋™์ผํ•˜๊ฒŒ `is_blank_description`/`is_forced_null_description` ๊ทœ์น™์œผ๋กœ ์ •๋ฆฌํ•˜๊ณ , ๊ณต๋ฐฑ/๊ฐœํ–‰์€ `format_string_literal`์— ๋งž๊ฒŒ ์œ ์ง€.
- ์ŠคํŽ™ ๋ณ‘ํ•ฉ: `field_specs.<code>.desc`์— ์ฃผ์ž…ํ•˜๊ณ  `field_aliases`์—๋„ ๋™์ผ desc ๋ณต์ œ. ๋งค์นญ ์‹คํŒจ(๋ฌธ์„œ์—๋งŒ ์žˆ๊ฑฐ๋‚˜ ์ŠคํŽ™์—๋งŒ ์žˆ๋Š” ์ฝ”๋“œ)๋Š” ์ด์Šˆ๋กœ ๊ธฐ๋ก.
- enum_desc(์„ ํƒ, API ์ •ํ•ฉ์„ฑ): API์˜ `parse_enum_entries` ๋กœ์ง์„ ์žฌ์‚ฌ์šฉํ•ด ์„ค๋ช…๋ฌธ ๋‚ด `1: โ€ฆ` ํŒจํ„ด์„ `enum_desc`๋กœ ์Šน๊ฒฉ(์ƒ์„ฑ ์‹œ `Field(json_schema_extra={"enum_desc": ...})`).
- ๋ฆฌํฌํŒ…: `tmp/docs/reports/streaming_docmap_report.json`์— ๋งค์นญ/๋ˆ„๋ฝ ํ†ต๊ณ„ ๋ฐ ๊ฒฝ๊ณ  ๊ธฐ๋ก(override๋Š” ์Šน์ธ ์ „ ๋ฏธ์ ์šฉ).
- ์žฌ์ƒ์„ฑ: streaming pipeline ์žฌ์‹คํ–‰ ํ›„ `src/pykis/streaming/raw/structs/*`์— `Annotated[..., """desc"""]` ์ถœ๋ ฅ ํ™•์ธ, ruff/ty ์žฌ์‹คํ–‰.

**Changes**
- `scripts/build_unasync.py`: API sync ํ›„์ฒ˜๋ฆฌ์—์„œ `threading`/`time` import ์‚ฝ์ž… ์ˆœ์„œ๋ฅผ ์ •๋ ฌํ•˜๋„๋ก ๋ณด์ •.
- `src/pykis/api/_sync/client.py`: unasync ์žฌ์ƒ์„ฑ ๊ฒฐ๊ณผ import ์ˆœ์„œ ์ •๋ ฌ.

**Checks**
- `ruff format .` (๊ฒฝ๊ณ : isort ์˜ต์…˜ ์ถฉ๋Œ)
- `ruff check .`
- `ty check .`

**Notes**
- `.venv/bin/python`์ด ์—†์–ด `python scripts/build_unasync.py`(Python 3.12.2)๋กœ unasync ์‹คํ–‰ํ–ˆ์–ด.
- ํ˜„์žฌ ์ˆ˜์ • ๋ฏธ์ปค๋ฐ‹: `scripts/build_unasync.py`, `src/pykis/api/_sync/client.py`.

๋‹ค์Œ์— ์ง„ํ–‰ํ• ๊นŒ์š”?
1) unasync ๋ณด์ •/์žฌ์ƒ์„ฑ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ปค๋ฐ‹
2) ์œ„ ๊ณ„ํš๋Œ€๋กœ streaming doc ์ถ”์ถœ/์ŠคํŽ™ ๋ณ‘ํ•ฉ ์ž‘์—… ์ฐฉ์ˆ˜
โ€ฆ์„ค๋ช…์„ spec์— ๋ณ‘ํ•ฉํ•ด codegen์— ๋ฐ˜์˜๋˜๋„๋ก ํŒŒ์ดํ”„๋ผ์ธ์„ ํ™•์žฅํ–ˆ์–ด. docstring์€ ๊ธฐ์กด์ฒ˜๋Ÿผ `"""docstring"""`์„ ์œ ์ง€ํ•˜๋„๋ก ๋ถ„๋ฆฌ ์ฒ˜๋ฆฌํ–ˆ์–ด.

- `format_annotated_type`๊ฐ€ ์ด์ œ ํ•ญ์ƒ ์ธ๋ผ์ธ ๋ฌธ์ž์—ด ๋ฆฌํ„ฐ๋Ÿด์„ ์“ฐ๋„๋ก ๋ณ€๊ฒฝํ–ˆ๊ณ , docstring์šฉ `format_string_literal`์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ–ˆ์–ด. `src/codegen/core/text.py`
- streaming ๋ฌธ์„œ์—์„œ `response.body`์˜ `code/description`์„ ์ˆ˜์ง‘ํ•ด `FieldSpec.desc`์™€ `enum_desc`์— ๋ณ‘ํ•ฉํ•˜๋Š” ๋‹จ๊ณ„ ์ถ”๊ฐ€ํ–ˆ์–ด. ๊ฒฐ๊ณผ ๋ฆฌํฌํŠธ๋Š” `tmp/docs/reports/streaming_docmap_report.json`๋กœ ์ €์žฅ๋ผ. `src/codegen/streaming/steps/docmap.py`, `src/codegen/streaming/pipeline.py`

Tests:
- `ruff format .` (isort ์˜ต์…˜ ๊ฒฝ๊ณ  ์ถœ๋ ฅ๋จ)
- `ruff check .`
- `ty check .`

๋ณ€๊ฒฝ ํŒŒ์ผ:
- `src/codegen/core/text.py`
- `src/codegen/streaming/pipeline.py`
- `src/codegen/streaming/steps/docmap.py`

๋‹ค์Œ ๋‹จ๊ณ„ ์ œ์•ˆ:
1) streaming codegen ์‹คํ–‰ํ•ด์„œ ์ƒˆ `Annotated` ์ถœ๋ ฅ๊ณผ `streaming_docmap_report.json` ํ™•์ธ
2) ํ•„์š”ํ•˜๋ฉด spec ์—…๋ฐ์ดํŠธ/๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ˆ„๋ฝ ํ•„๋“œ ์ •๋ฆฌ
Implement 2-layer architecture (L0 Raw โ†’ L1 Service) replacing the
previous 3-layer design (ADR-012). Includes infrastructure (enums,
enum maps, conversion helpers), Quote domain end-to-end (models,
service orchestration, domestic/overseas mappers), L0 drift detection
script with pre-commit hook, and /service-build agent skill.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant