feat: python-kis v3 โ 2-layer architecture with codegen pipeline, streaming, and service layer#85
Open
feat: python-kis v3 โ 2-layer architecture with codegen pipeline, streaming, and service layer#85
Conversation
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).
โฆ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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
python-kis๋ฅผ v3๋ก ์ ๋ฉด ์ฌ์ค๊ณํฉ๋๋ค. KIS (ํ๊ตญํฌ์์ฆ๊ถ) Open API์ ๊ตญ๋ด/ํด์ธ ๊ฐ ๊ตฌ์กฐ์ ์ฐจ์ด(ํ๋๋ช , enum ์ฝ๋, ์๋ต ๊ตฌ์กฐ, ํธ์ถ ํจํด)๋ฅผ ๊ทน๋ณตํ๊ธฐ ์ํด, 2-Layer Architecture ๊ธฐ๋ฐ์ ์๋ก์ด SDK๋ฅผ ์ฒ์๋ถํฐ ๊ตฌ์ถํ์ต๋๋ค.
ํต์ฌ ๋ณ๊ฒฝ
Architecture
์ 2-Layer์ธ๊ฐ? (ADR-012)
v2 ๋ถ์ ๊ฒฐ๊ณผ, ๊ตญ๋ด/ํด์ธ ํตํฉ์ ๋จ์ ํ๋ ๋งคํ์ด ์๋ ์ ์ฐจ์ ์๋น์ค ๋ก์ง์ ์๊ตฌํฉ๋๋ค:
exchange.is_domesticโ ๋ค๋ฅธ endpoint ํธ์ถ๊ฒฐ๋ก : L1 Normalize + L2 System์ L1 Service๋ก ํตํฉ. YAML์ "what" (๋ชจ๋ธ ์ ์), Python์ "how" (๋ณํ ๋ก์ง)์ ๋ด๋นํฉ๋๋ค.
Module Breakdown
1. Codegen Pipeline (
src/codegen/, 67 files)KIS API ๋ฌธ์(YAML/MD)์์ ํ์ ์์ ํ Pydantic ๋ชจ๋ธ์ ์๋ ์์ฑํฉ๋๋ค.
ํ์ดํ๋ผ์ธ ํ์ง ๊ฒ์ดํธ:
smoke.errors: 0โ ํ์ฑ ์ค๋ฅ ์์validation.errors: 0โ ํ์ /์คํค๋ง ๊ฒ์ฆ ํต๊ณผaugment.errors: 0โ enum/ํ์ ์ถ๋ก ์ค๋ฅ ์์2. L0 Raw Models (
src/pykis/api/raw/, 294 files)alias๋ก ํ๊ธ ์๋ณธ ํ๋๋ช ์ ์ง, Python snake_case ์ ๊ทผextra="ignore"๋ก ๋ฏธ๋ ํ๋์ ๋ํ ์ ๋ฐฉ ํธํ์ฑregistry.py)3. Streaming System (
src/pykis/streaming/, 71 files)์ค์๊ฐ WebSocket ์์ธ๋ฅผ 3๋จ๊ณ๋ก ์ฒ๋ฆฌํฉ๋๋ค:
NormalizedTrade (ํตํฉ ์์ธ ๋ชจ๋ธ):
symbol,market,timestamp_utcprice,volume,open,high,low,close(allDecimal)change,change_rate,change_signbid_price,ask_price,bid_size,ask_sizeNormalizedOrderBook (ํตํฉ ํธ๊ฐ ๋ชจ๋ธ):
4. L1 Service Layer (
src/pykis/service/, 10 files)๊ตญ๋ด/ํด์ธ๋ฅผ ๋จ์ผ ์ธํฐํ์ด์ค๋ก ํตํฉํ๋ ๋๋ฉ์ธ ์๋น์ค ๊ณ์ธต์ ๋๋ค.
Infrastructure
enums.pyExchange(16๊ฐ),ChangeSign(5๊ฐ),MarketWarning(4๊ฐ)_enum_map.py_convert.pyDecimal๋ณํ ํฌํผ (decimal_required,decimal_optional)_types.pyMarket = Literal["domestic", "overseas"]Exchange Enum (ํตํฉ ๊ฑฐ๋์)
Quote Domain (์ฒซ ๋ฒ์งธ vertical slice)
Quote ๋ชจ๋ธ โ ๊ตญ๋ด/ํด์ธ ํตํฉ ํ์ฌ๊ฐ:
์๋น์ค ๋ผ์ฐํ ํจํด:
Protocol ๊ธฐ๋ฐ ๋งคํผ โ L0 ์ฝ๋์์ ๊ฒฐํฉ๋๋ฅผ ์ต์ํ:
5. L0 Drift Detection (
scripts/check_l0_drift.py)codegen์ด L0 ๋ชจ๋ธ์ ์ฌ์์ฑํ๋ฉด L1 Service์์ ์ ํฉ์ฑ์ด ๊นจ์ง ์ ์์ต๋๋ค.
๊ตฌ์กฐ์ ์๊ทธ๋์ฒ ๋น๊ต๋ก ๋ณ๋์ ๊ฐ์งํฉ๋๋ค.
์๊ทธ๋์ฒ ๊ณ์ฐ
ํด์์ ํฌํจ (structural shape):
ํด์์ ๋ฏธํฌํจ (documentation):
๋งค๋ํ์คํธ (
data/service/_l0_manifest.yaml)268๊ฐ endpoint์ ๊ตฌ์กฐ์ ์๊ทธ๋์ฒ๋ฅผ ์ถ์ ํฉ๋๋ค.
Pre-commit Hook ์ฐ๋
6. Store Module (
src/pykis/store/, 64 files)SQLite ๊ธฐ๋ฐ ๋ง์คํฐ ๋ฐ์ดํฐ(์ข ๋ชฉ ์ฝ๋, ๊ฑฐ๋์ ์ ๋ณด ๋ฑ) ๊ด๋ฆฌ.
7. Transport & Client (
src/pykis/client/)/_async/client.py๊ฐ canonical sourcescripts/build_unasync.py๊ฐ/_sync/์์ฑasyncio.Lockโthreading.Lock์๋ ๋ณํSpecifications & Governance
specs/project.mdspecs/stakeholder-requirements.mdspecs/srs/specs/architecture/layered-model.mdspecs/architecture/adr/ADR-012-service-layer.mdspecs/governance/Agent Skills
/service-build/pipeline-run/override-review/api-audit/explore-kis-apiStats
Test Plan
pytestโ 140 tests passed (11.66s)ruff checkโ cleanty checkโ cleanNext Steps (Future PRs)
KisClient/KisAsyncClient)