장마감이후 호출하는 경우 KeyError에 대한 방어코드 추가함. - #76
Open
visualmoney wants to merge 209 commits into
Open
Conversation
…래스가 정상 동작하는지 확인 3. 지 않은 서브클래스는 여전히 추상으로 취급되는지 확인)
…를 조정하는 리팩토링 (test_cache.py 수정완료)
정식 배포 직전 마이그레이션 산출물을 재검토했다. 배포를 막아야 하는
결함 하나와, 문서 여러 편이 일어난 적 없는 릴리스 이력을 서술하는
문제가 나왔다.
## 설치 안내가 존재하지 않는 배포명을 가리켰다
문서 11곳이 `pip install vmkis` 라고 안내했다. vmkis 는 모듈명이고
배포명은 vm-stock-kis 다. PyPI에서 vmkis 는 404이며 누구나 선점할 수
있다. 선점되는 순간 우리 공식 문서가 제3자 패키지 설치를 안내하게
된다. 증권 API 자격증명을 다루는 라이브러리에서 가벼운 문제가 아니다.
이슈 #2의 스윕이 이걸 더 나쁘게 만들었다. 스윕 전에는
`pip install pykis`(명백히 남의 패키지)였는데 `\bpykis\b` -> vmkis
규칙이 우리 모듈명과 같게 만들어 그럴듯해졌다.
## 버전 3.0.0 -> 0.0.1
vm-stock-kis 는 PyPI에 존재한 적이 없다. 이번이 첫 릴리스다.
3.0.0 은 업스트림 2.1.6을 이어받은 숫자였지만, 배포명이 다르면 pip은
두 버전을 비교하지 않는다. 이어받을 이유가 없고 실제보다 성숙해
보이게 만든다.
1차 정식 v3.0.0 -> 0.0.1
호환 shim 제거 v4.0.0 -> 1.0.0
Development Status 5 - Production/Stable -> 4 - Beta
classifier 를 함께 내린 것은 0.0.1과 Production/Stable 이 함께 설 수
없기 때문이다. 1.0.0에서 되돌린다.
버전 표기는 문서에만 있지 않았다. PyKis 별칭 / PYKIS_* / ~/.pykis
폴백의 DeprecationWarning 문구와 그것을 단언하는 테스트도 갱신했다.
사용자가 실제로 읽는 것은 이 문자열이다.
## MIGRATION_GUIDE 는 재작성
v2.1.7 -> v2.2.0 -> v3.0.0 3단 구성으로 쓰여 있었으나 그런 릴리스는
존재하지 않았다. "v2.2.0 변경사항"으로 서술된 작업은 전부 미배포이며
0.0.1에 함께 실린다. 게다가 스윕이 v2.x 시절 예제까지 새 이름으로
바꿔 놓아 문서가 스스로를 반박하고 있었다 — 비교표는 세 열이 전부
`from vmkis import ...` 라 아무것도 비교하지 못했다.
코드에 대조하다 사실 오류 셋을 찾았다.
SimpleKIS(config_path=...) 실제 생성자는 VmKis 인스턴스를 받는다.
문서대로 하면 TypeError 다.
MarketInfo = KisMarketInfo 실제는 KisMarketType
공개 API 20개 __all__ 은 12개
## API_STABILITY_POLICY 의 가공된 이력
"v1.x END-OF-LIFE / v2.x 12개월 지원 / v3.0-beta 2026-01~2027-01"
같은 표가 있었다. 그런 이력도 지원 약속도 없다. 지원 기간은
"정하지 않았다"고 명시했다 — 지킬 수 없는 약속을 적는 것보다 낫다.
의존성 표도 pyproject.toml 과 어긋나 있어 고치고 출처를 명시했다.
## Python KIS
#2의 스윕은 붙임표가 있는 Python-KIS 만 찾았다. 붙임표 없는 표기가
5곳 남아 있었고 그중 4곳이 문서의 H1 제목이었다.
v0.0.1 태그는 붙이지 않는다.
Refs #25, #2
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
이슈 #2가 "TestPyPI에서 2.5가 통과하면 이 두 줄을 삭제하세요"라고
남긴 항목을 검토했다. 결론은 고정 유지다. 다만 이유가 바뀌었다.
원래 근거였던 "PyPI가 2.5를 받는지 모른다"는 해소됐다. warehouse의
업로드 검증이 2.5를 받는다.
SUPPORTED_METADATA_VERSIONS =
{"1.0","1.1","1.2","2.1","2.2","2.3","2.4","2.5"}
그런데 그것이 고정을 풀 이유는 되지 않는다.
올려도 얻는 것이 없다. PEP 794가 2.5에서 추가한 필드는 Import-Name 과
Import-Namespace 둘뿐인데 hatchling 이 이 둘을 쓰지 않는다. 실제로
2.5로 빌드한 휠에 Import-Name 이 없다. 우리에게 2.4와 2.5는 내용이
같고 버전 숫자만 다르다.
정작 위험은 다음 버전이다. core metadata 2.6이 2026-05에 승인됐지만
위 목록에 없다 — PyPI가 아직 받지 않는다. hatchling 은 1.32.0에서
기본값을 2.4 -> 2.5로 이미 한 번 올렸다. 같은 일이 2.6으로 또 나면
고정이 없는 쪽이 배포에 실패한다.
즉 고정의 목적은 "수용 여부를 몰라서"가 아니라 "빌드 백엔드 기본값이
우리 모르게 바뀌는 것을 막는 것"이다. 남는 두 줄은 같지만 이유가
다르므로 주석을 바꿨다. 특히 "삭제하세요"라는 지시가 위험하다.
publish.yml 의 Wheel contents 스텝에 검사를 넣었다. twine check 는
형식만 보고 PyPI가 그 버전을 받는지는 모른다. 고정이 실수로 지워지거나
백엔드가 기본값을 올려도 게시 시도 전에 잡힌다.
검증 — 스텝 스크립트를 워크플로에서 뽑아 직접 실행:
A 현행 2.4 아티팩트 통과 (exit=0)
B 고정 삭제 -> 2.5 통과 (exit=0)
C core-metadata-version="2.6" hatchling 이 빌드 단계에서 거부
D 2.6으로 다시 포장한 휠 실패 (exit=1)
C가 통과/실패 어느 쪽도 아닌 것은 hatchling 1.32.0이 아직 2.6을 낼 수
없기 때문이다. 그래서 실제 위험 시나리오를 재현하려고 METADATA만 고쳐
다시 포장한 휠로 D를 만들었다.
Closes #27
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
tests/performance/ 30개 중 8개만 performance 마커를 갖고 있었다. test_benchmark.py 마커 0 / 테스트 7 test_memory.py 마커 0 / 테스트 7 test_websocket_stress.py 마커 0 / 테스트 8 test_performance_advanced.py 마커 3 / 테스트 7 test_perf_dummy.py 마커 1 / 테스트 1 즉 게이팅 잡(-m 'not requires_api')이 성능 테스트 22개를 그대로 수집하고 있었고, 그중 test_benchmark.py 는 이슈 #23 의 시계 해상도 flake다. 지금 CI가 초록인 것은 러너가 느려서일 뿐이고 러너 세대가 바뀌면 main 이 red 가 될 상태였다. 코드와 무관한 이유로 머지가 막힌다. 파일마다 마커를 붙이는 방식은 이미 한 번 실패했으므로(5개 중 3개 누락) tests/performance/conftest.py 로 디렉터리 규칙을 둔다. 새 파일이 마커 없이 추가돼도 반복되지 않는다. 함정 하나를 밟았다. 하위 디렉터리의 conftest 라도 pytest_collection_modifyitems 는 수집된 전체 목록을 받는다. 경로로 거르지 않은 첫 시도에서 저장소의 모든 테스트가 performance 로 표시되어 게이팅 잡이 아무것도 수집하지 않았다(991 deselected). 검증 없이 넘어갔다면 CI가 초록인 채로 테스트를 하나도 돌리지 않았을 것이다. 경로 필터를 넣고 합을 확인했다: 944 + 30 = 974. 커버리지 영향은 먼저 실측했다. 게이트가 90인데 성능 테스트를 빼서 깨지면 안 되기 때문이다. 성능 포함 TOTAL 90.73% 성능 제외 TOTAL 90.72% 0.01%p. 성능 테스트는 커버리지에 사실상 기여하지 않는다. 성능 잡은 continue-on-error 로 두고 ci-ok 의 needs 에 넣지 않는다. 아예 돌리지 않으면 성능 회귀를 영영 못 보므로 돌리되 막지 않는다. --cov 는 주지 않는다. coverage 의 trace 함수가 측정을 느리게 만들어 성능 수치를 왜곡한다. 동작 확인: 게이팅 937 passed, 47 deselected, TOTAL 90.72% 비차단 3 failed, 26 passed -> ci-ok 는 통과 #23 의 근본 수정(time.time() -> time.perf_counter() 18곳)은 별건이다. Refs #23 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
템플릿 3개가 한국어 라벨을 붙이려 하는데 저장소에는 그 라벨이 없었다. GitHub은 이슈 폼의 labels: 에 없는 이름이 있으면 조용히 버리고 만들어주지 않는다. 즉 템플릿으로 들어온 이슈는 전부 무라벨로 생성되고 있었다. bug-report.yml "버그" -> "bug" feature-request.yml "기능" -> "enhancement" question.yml "질문" -> "question" 기존 기본 라벨과 이름만 한국어로 다를 뿐이라 라벨을 새로 만들지 않고 템플릿 쪽을 맞췄다. 라벨을 늘리지 않는 방향이다. dependabot.yml 이 참조하는 dependencies 라벨도 같은 이유로 끊겨 있었다. 이쪽은 참조가 옳고 라벨이 없던 것이라 라벨을 만들어 복구했다(파일 변경 없음). bug-report.yml 의 진단 출력 예시가 "Version: VmKis/2.0.0" 이었다. 그런 버전은 존재한 적이 없다. 첫 릴리스인 0.0.1 로 고쳤다. 검증 — 저장소 설정이 참조하는 모든 라벨이 실재하는지 대조: OK bug-report.yml -> 'bug' OK feature-request.yml -> 'enhancement' OK question.yml -> 'question' OK dependabot.yml -> 'dependencies' Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## #14 — request() 가 영원히 반환되지 않을 수 있었다 kis.py 의 while True 루프에 재시도 상한이 없었다. if error_code == "EGW00201": # 초당 호출 초과 sleep(0.1) continue # 상한 없음 서버가 EGW00201(유량 초과)이나 EGW00123(토큰 만료)을 계속 반환하면 호출이 반환되지 않는다. 타임아웃도 예외도 없다. 자동매매에서 이건 "느리다"가 아니라 "멈춘다"이다. 게다가 0.1초 고정 간격 재시도는 유량 제한 상황을 악화시킨다. 유량 초과 최대 5회, 지수 백오프 + 지터 (0.1s 시작, 상한 5s) 소진 시 KisRateLimitError 토큰 만료 재발급 1회. 재발급 후에도 같은 오류면 만료가 아니라 인증 문제이므로 KisAuthenticationError 로 즉시 실패 최악의 경우 대기는 약 3.1초이고 그 뒤 예외로 끝난다. 조용히 매달려 있는 것보다 낫다. 더 기다려야 하는 호출자는 상위에서 재시도하면 된다. utils/retry.py 의 RetryConfig 를 재사용하되 **모듈 전역 retry_config 싱글턴은 쓰지 않는다.** with_retry 데코레이터가 그 싱글턴을 제자리에서 변형하기 때문에(config = retry_config 후 대입) 공유하면 데코레이터를 한 번 쓰는 순간 이쪽 정책까지 바뀐다. 전용 인스턴스를 둔다. 정책 값은 __env__.py 에 모았다. 기존 유량 제한 상수와 같은 자리다. ## #16 — KisPage 가 커서 접미사 2종만 파싱했다 CTX_AREA_FK 에는 4가지 변형이 있다. 공식 샘플 274개 REST API 전수 조사 기준 FK100 15 / FK200 25 / FK 2 / FK50 1. 접미사 없는 CTX_AREA_FK 를 쓰는 API(예: 국내휴장일조회 CTCA0903R)는 KisPaginationAPIResponse 를 상속하는 순간 파싱 단계에서 죽었다. 사용자가 페이징 프레임워크를 못 쓰고 커서 루프를 손으로 짜야 했다. 접미사 없는 변형을 표현하려고 size 에 NO_SUFFIX(0) 를 도입했다. size 는 필드명에 붙는 숫자를 그대로 담으므로 "숫자 없음"이 0 이다. 커서 길이가 0 이라는 뜻이 아니며, to() 의 길이 검사에서도 제외한다. 검사하면 접미사 없는 커서를 파싱한 뒤 to(NO_SUFFIX) 가 항상 실패한다. ## 테스트 재시도 테스트는 return_value 가 아니라 유한한 side_effect 목록을 쓴다. return_value 로 두면 상한이 회귀했을 때 테스트가 실패가 아니라 무한 정지한다. CI를 멈추게 하는 것은 빨간 줄보다 나쁘다. 954 passed, 7 skipped (게이팅) TOTAL 90.75% (게이트 90) Closes #14, #16 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## #20 — 문서가 코드에 대해 사실이 아닌 것을 말하고 있었다 착수 전 7건을 각각 실측했다. 전부 지금도 유효했다. 계층 다이어그램(API -> Client -> Response Transform -> Utility)이 코드와 맞지 않는다. AST 로 전 파일 import 를 분류하니 런타임 모듈레벨 역방향이 12건이다(TYPE_CHECKING 24건은 런타임 의존이 아니라 제외). 12건을 간선 종류로 묶으면 비교 보고서의 7종과 일치한다 — 이슈가 틀린 게 아니라 종류를 센 것이다. 더 큰 문제는 다이어그램에 event/ 가 아예 없다는 것이었다. client -> event 3건과 event -> api 3건은 위반인지 아닌지 판정 자체가 불가능했다. 허브-스포크 그림으로 교체하고 **불변식을 명문화**했다. 이게 이 작업의 핵심이다. 1. vmkis.kis 를 모듈 레벨에서 import 금지 — 전체 패키지가 정상 로드되는 유일한 이유인데 어디에도 없었다 2. 새 모듈레벨 역방향 금지. 기존은 "의도적/정리 대상"으로 분류해 동결 3. 순환 우회 지연 import 에 사유 주석 필수 (현재 0곳) 4. event/ 를 그림에 포함 3번이 없으면 실제로 위험하다. 린터가 함수 안 import 를 위로 올리라고 권하는데 그대로 따르면 패키지가 로드 불능이 된다. 함께 고친 드리프트: 모의 Rate Limit 초당 1개 -> 2개 (출처가 __env__.py 임을 명시) types.py 설명 "공개 타입 정의" -> 고급 100개 / public_types 9개 새 API 추가 4단계 -> 6단계 250~800 LOC WebSocket 이벤트 4단계 -> 5단계, 레지스트리 등록을 경고로 강조 문서 버전 2.1.7 -> 0.0.1 "v2.2.0+" 서술 존재한 적 없는 릴리스 -> 포크 이후 정리 내용 CLAUDE.md 가 CODING_STANDARDS/GIT_WORKFLOW/DOCUMENTATION_RULES 를 참조하는데 셋 다 없다. AI 개발 가이드가 존재하지 않는 규칙 문서를 가리키고 있었다. 실제 목록으로 교체했다. ARCHITECTURE_QUALITY_KR.md 는 옮기지도 고치지도 않고 경고를 달았다. pykis/ 경로 22곳이 남아 있는데, 이는 복잡도/커버리지/테스트 수를 전부 다른 트리에서 쟀다는 뜻이다. 경로만 치환하면 틀린 숫자가 맞는 것처럼 보이게 될 뿐이다. archive 이동은 다른 보고서 4곳이 링크 중이라 보류했다. ## #19 — 이미 있는 기능이 문서에 없었다 VmKis.fetch() 는 완성도 높은 escape hatch 인데 사용자 문서 어디에도 없었다. 이 라이브러리는 74 TR 만 지원하고 공식 샘플은 377 TR 이다. 전부 손으로 구현하는 것은 비현실적이므로, "vmkis 로 시작하고 없는 TR 은 fetch() 로 뚫는다"는 사용 모델을 공식화한다. docs/user/EXTENDING_API.md 신설. Level 0(5줄) / Level 1(30~60줄) / Level 2(통합) / Level 3(실시간) + 함정 11개. 비교 보고서를 그대로 옮기지 않았다. 보고서 함정표에 이미 고친 것이 남아 있었다. "EGW00201 시 상한 없는 재시도 루프" -> #14 에서 상한·백오프 추가됨 "커서 fk100 vs fk200" -> #16 에서 fk50·접미사없음 지원 그대로 옮겼다면 오늘 고친 것을 틀리게 문서화할 뻔했다. 문서의 Level 1 예제를 인터프리터에 그대로 넣어 클래스 정의가 성립하는 것을 확인했다. 검증 중 KisDynamicDict 를 responses.dynamic 에서 찾다 실패했는데 실제 위치는 responses.types 였다. 본문이 모듈 경로를 명시하지 않아 영향은 없었다. README 의 "빠른 시작"에 진입점을 넣었다. 미지원 TR 을 만난 사용자가 이슈를 열기 전에 이 문서를 만나는 것이 목적이다. Closes #19, Closes #20 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
세 건 다 "코드는 멀쩡한데 도구가 거짓을 말하는" 부류였다. ## #23 — 빠를수록 실패하던 테스트 원인이 두 겹이었다. time.time() 은 벽시계다. Windows 눈금이 약 15.6ms 인데 측정 구간이 그보다 빨리 끝나면 경과가 정확히 0.000s 로 찍힌다. 그리고 그때 ops_per_second 가 0.0 을 반환했다. if self.elapsed > 0: return self.count / self.elapsed return 0.0 # "측정 불가능하게 빨랐다" 를 "처리량 0" 으로 보고 assert ops_per_second > 10 이 성능이 좋을 때 실패한다. 검사 방향이 뒤집혀 있었다. time.time() 18곳을 perf_counter 로 바꾸고 0 반환을 inf 로 고쳤다. perf_counter 는 단조 증가하고 해상도가 높으며 NTP 동기화·서머타임의 영향도 받지 않는다. 같은 버그를 우회하던 죽은 단언도 찾았다. if elapsed > 0: assert benchmark.ops_per_second > 0 else: assert True # 아무것도 검사하지 않는다 우회가 필요 없어졌으므로 실제 검사로 바꿨다. 범위는 test_benchmark.py 로 좁혔다. time.time() 은 tests/ 전체에 52곳 이지만 나머지는 초 단위 측정이거나 상한 검사라 해상도가 문제되지 않는다. performance/conftest.py 의 것은 코드가 아니라 내가 쓴 docstring 이다. 검증: 5회 연속 7 passed (이전엔 실행마다 1~4개 무작위 실패). ## #38 — 첫 실행에서 17개가 빨갛게 뜨던 문제 새로 클론한 사람이 pytest 를 처음 돌리면 17 errors 를 봤다. 코드는 멀쩡하고 자격증명이 없을 뿐이다. failed 가 아니라 error 인 것은 setUpClass 에서 생성자가 ValueError 를 냈기 때문이다. load_vmkis() 가 유일한 관문이므로 거기에 검사를 넣었다. unittest 는 setUpClass 의 SkipTest 를 받아 클래스 전체를 건너뛴다. addopts 에 -m 'not requires_api' 를 넣는 방법은 택하지 않았다. 조용히 동작해서 "왜 17개가 안 돌지"로 문제가 바뀔 뿐이다. skip 은 사유를 화면에 남긴다. 소멸자도 함께 고쳤다. 생성자가 중간에 실패하면 _sessions 가 설정되기 전에 __del__ -> close() 가 그것을 참조해 AttributeError 를 냈다. tests/unit/test_kis.py:96 이 이미 소멸자를 무력화하는 패치로 우회하고 있었다 — 테스트가 프로덕션 결함을 우회하고 있으면 그 결함을 고치는 게 맞다. 함께 발견: test_product_quote.py 가 load_vmkis("mock") 을 부르고 "hermetic 하다"고 주석을 달았는데 그런 도메인은 없다. else 분기로 떨어져 결국 자격증명을 요구했다. 주석이 사실이 아니었다. 결과: 983 passed, 25 skipped, ERROR 0, Unraisable 경고 0. ## #29 — 문서 인덱스가 작성자 PC를 가리키고 있었다 417줄 INDEX.md 의 링크 28곳이 로컬 절대경로였고 그것도 포크 이전 디렉터리명이었다. GitHub 에서 전부 죽은 링크이고 클론한 사람의 디스크 에도 없다. 트리 블록이 섞이고 표가 깨지고 없는 경로를 안내했다. git ls-files 로 실제 구조를 뽑아 다시 썼다. 마지막에 "현재 값은 문서가 아니라 코드에서" 절을 넣었다 — 버전·의존성·Rate Limit·공개 API·커버리지의 살아 있는 출처를 표로 적었다. 문서에 값을 베껴 적으면 다시 드리프트한다. 검증: 상대 링크 40개 전부 실재, 로컬 절대경로 0. 남은 18곳은 전부 기록물이라 의도적으로 두었다. Closes #23, Closes #29, Closes #38 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 전역 싱글턴을 제자리에서 변형하고 있었다
config = retry_config # 사본이 아니라 전역 객체 그 자체
if max_retries is not None:
config.max_retries = max_retries
@with_retry(max_retries=7, initial_delay=9.0) 를 한 번 쓰면 전역이
바뀌고, 그 뒤로는 인자 없는 @with_retry() 까지 7회·9초로 동작했다.
호출 순서에 따라 달라져 재현이 어려웠다.
_resolve_config() 를 두어 전역을 읽기만 하고 새 인스턴스를 만든다.
동기·비동기 두 데코레이터 모두 적용했다.
커버리지 95%인데 못 잡은 이유는 미스 분기가 정확히 이 경로였기
때문이다. 126->128 은 max_retries is None 일 때 건너뛰는 분기인데,
어떤 테스트도 with_retry() 를 인자 없이 부른 적이 없었다.
## utils -> client 계층 위반
utils/retry.py 가 재시도 대상 예외 목록을 들고 있느라 상위 계층을
import 했다. utils 에서 상위를 참조하는 유일한 지점이었다.
이슈는 "예외를 파라미터로 주입" 또는 "예외 정의를 하위 모듈로 이동"을
제안했으나 셋째 길을 택했다. 판단 근거를 예외 자신에게 넘긴다.
class KisException(Exception):
retryable: bool = False
class KisRateLimitError(KisHTTPError):
retryable = True
# utils/retry.py — vmkis 를 아무것도 import 하지 않음
def is_retryable(exc): return getattr(exc, "retryable", False) is True
목록을 옮기면 "어디에 두느냐" 문제가 남는다. 예외가 스스로 표식을
들고 있으면 유틸은 아무것도 알 필요가 없고, 새 예외를 만드는 사람이
그 자리에서 선언하므로 목록 갱신을 잊을 일도 없다.
파라미터 주입을 택하지 않은 이유: on= 을 필수로 하면 breaking 이고,
기본값을 () 로 두면 아무것도 재시도하지 않는 쪽으로 조용히 바뀐다.
증권 API 에서 그 실패 모드는 위험하다.
except 절이 타입 튜플에서 표식 검사로 넓어졌으나, 대상이 아니면
즉시 raise 하므로 동작은 같다. 테스트로 고정했다.
## 죽은 코드를 살렸다
KisRetryableError 는 "재시도 가능 여부를 나타내는 인터페이스"라고
적혀 있으면서 아무것도 상속하지 않고, 발생시키지도 잡지도 않으면서
__all__ 에만 두 곳 있었다. KisException 과 별개 트리라 재시도 판단에
쓰이지도 않았다. retryable = True 를 달아 실제로 의미를 갖게 했다.
## 회귀 테스트가 버그를 잡는지 확인했다
테스트 추가 후 버그를 일부러 되살려 실패하는 것을 확인하고 복원했다.
test_retry_module_imports_nothing_from_vmkis 는 AST 로 import 를
검사한다. 누가 편의상 import 를 되살리면 실패한다. import-linter 도입
전까지의 경량 대체재다.
런타임 모듈레벨 역방향 의존 12건 -> 11건.
Closes #18
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
이슈가 서술한 것보다 심각했다. "어느 것을 import 했는지에 따라 다르게
동작한다"가 아니라, 공개 API 를 따른 사용자의 핸들러가 절대 실행되지
않았다.
vmkis.exceptions.KisNotFoundError 는: vmkis.client.exceptions
실제로 raise 되는 것 : vmkis.responses.exceptions
둘이 같은가 : False
vmkis/exceptions.py 가 client.exceptions 를 통째로 import 하면서
KisNotFoundError 도 딸려 왔다. 약 50개 docstring 이
"Raises: KisNotFoundError: 조회 결과가 없는 경우" 라고 안내하는데,
공개 모듈에서 그 이름을 가져오면 다른 클래스를 잡게 된다.
from vmkis.exceptions import KisNotFoundError
try:
kis.stock("005930").quote()
except KisNotFoundError: # 절대 잡히지 않음
...
## 이슈의 제안과 반대 방향을 택했다
이슈는 "사용 빈도 조사 후 결정"하라고 했다. 재보니 한쪽이 완전히
죽어 있었다.
responses(조회결과없음) client(HTTP 404)
raise 되는 곳 response.py:41 0곳
import 하는 곳 src 2 + tests 3 0곳
docstring 언급 약 50곳 0곳
이슈는 살아 있는 쪽(responses)을 개명하자고 했으나, 죽은 쪽(client)을
개명하는 것으로 뒤집었다.
개명 대상 이슈: responses 채택: client
docstring 수정 약 50곳 0곳
공개 모듈 여전히 안 잡힘 실제 발생 클래스
client 쪽은 raise 0회 / import 0곳이라 개명해도 깨질 코드가 없다.
그리고 KisNotFoundError 라는 이름은 실제로 그 상황에서 발생하는
예외가 가져가는 것이 맞다.
## 조치
client.exceptions.KisNotFoundError -> KisHTTPNotFoundError
vmkis/exceptions.py 가 responses 쪽을 KisNotFoundError 로 노출
KisHTTPNotFoundError 도 함께 노출
옛 경로는 모듈 __getattr__ 로 DeprecationWarning (1.0.0에서 제거)
두 클래스 docstring 에 차이를 표로 명시
별칭을 __all__ 에 넣지 않았다. import * 가 옛 이름을 퍼뜨린다.
PyKis -> VmKis 때와 같은 판단이다.
KisHTTPNotFoundError 는 여전히 아무도 발생시키지 않는다. kis.py 가
HTTP 상태 코드별로 예외를 세분화하지 않기 때문이며, 이는 별건이다.
CHANGELOG 에 [미출시] 절을 새로 열었다. 0.0.1 은 이미 배포됐다.
Closes #15
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
REST TR ID 삼항 분기 9곳 -> 0곳.
## KisEndpoint + VmKis.call()
흩어져 있던 규칙 셋을 한 곳에 모은다.
1. 실전/모의 TR ID 선택
2. 도메인 라우팅 — 모의 미지원 TR 은 실전으로
3. 커서 길이와 연속조회
2번이 핵심이다. tr_virtual 을 생략하는 것만으로 "모의 미지원 TR"이
표현되고 라우팅이 자동이다. 예전에는 domain="real" 을 손으로 붙였고
빠뜨리면 모의 계정에서만 터지는 버그가 됐다.
frozen=True 로 실행 중 변경을 막았다.
## 주문 계열부터 이관해 필드 설계를 검증했다
표는 KisEndpoint 보다 차원이 많았다.
DOMESTIC_ORDER_API_CODES: dict[tuple[bool, ORDER_TYPE], str]
FOREIGN_ORDER_API_CODES: dict[tuple[bool, MARKET_TYPE, ORDER_TYPE], str]
해법은 차원을 나누는 것이었다. 실전/모의만 스펙 안으로 넣고 나머지는
dict 키로 남긴다.
DOMESTIC_ORDER_ENDPOINTS: dict[ORDER_TYPE, KisEndpoint]
FOREIGN_ORDER_ENDPOINTS: dict[tuple[MARKET_TYPE, ORDER_TYPE], KisEndpoint]
18개 (시장, 매수/매도) 조합이 전부 실전/모의 쌍을 완비하고 있어 손실
없이 분해됐다. 18개를 손으로 전사하면 오타가 나므로 기존 표를 런타임에
읽어 새 리터럴을 생성했고, 쌍이 불완전한 조합이 없음을 함께 검증했다.
원본의 시장 설명 주석도 보존했다.
## 계좌 계열 이관
order.py 2곳 (스펙 20개)
balance.py 3곳
daily_order.py 1곳
order_modify.py 2곳
orderable_amount.py 2곳
pending_order.py 1곳
페이징이 특히 줄었다. page.to(100) / continuous=not page.is_first 를
호출부에서 없앴다.
## 테스트 — 단언의 가치를 지켰다
목이 fetch 를 잡고 있어 전부 깨졌다. 단언을 call(스펙) 으로 바꾸면
"국내 매수는 TTTC0802U 로 나간다"는 검증이 사라진다. 대신 목에 실제
VmKis.call 을 바인딩해, fetch(api=...) 단언을 살리면서 스펙 해석까지
함께 검증하게 했다. 테스트가 이전보다 더 많이 검증한다.
표 검증 테스트는 네트워크 없이 규칙을 확인하는 형태로 다시 썼다.
assert buy.resolve(virtual=False) == ("TTTC0802U", "real")
assert buy.resolve(virtual=True) == ("VTTC0802U", "virtual")
## 남은 것
api/stock/* 의 domain="real" 10곳. 전부 고정 TR 이라 tr_virtual 을
생략한 스펙으로 옮기면 domain 인자가 사라진다. 시세/차트 경로는
테스트가 많아 별도로 진행한다.
Refs #43
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
client/websocket.py 가 상위 계층인 api/websocket 을 모듈 레벨에서
import 하고 있었다. 통신 계층은 가장 안정적이어야 하는데, TR 하나를
추가할 때마다 client 까지 함께 바뀌었다.
## 레지스트리를 responses/ 에 두었다
이슈는 "client 가 소유하고 api 가 자기등록"을 제안했으나 한 단계 더
내렸다.
api/websocket/ (이전) client 에서 역방향
client/websocket.py 양쪽 정방향 (이슈 제안)
responses/websocket.py 양쪽 정방향 (채택)
client/websocket.py 는 이미 responses.websocket 을 import 하고
있었으므로 새 import 간선이 하나도 생기지 않는다. "TR ID -> 응답
클래스"는 응답 도메인 지식이라 의미상으로도 맞다.
## 하드코딩 튜플도 없앴다
# 이전 — client/websocket.py
if tr.id in ("H0STCNI0", "H0STCNI9", "H0GSCNI0", "H0GSCNI9"):
암호화 TR 을 추가할 때 이 파일도 고쳐야 했다. 이제 응답 클래스가
encrypted=True 로 선언하고 ENCRYPTED_TR_IDS 가 자동으로 채워진다.
## 가장 위험했던 지점 — 등록 시점
client/websocket.py:19 가 vmkis.api.websocket 을 import 하는 유일한
곳이었다. 그냥 지우면 응답 클래스가 로드되지 않아 레지스트리가 비고,
모든 실시간 이벤트가 조용히 사라진다. 이 이슈가 없애려던 바로 그
실패 모드다.
지운 뒤에도 동작하길래 왜인지 추적했다.
vmkis/__init__ -> vmkis.kis -> (클래스 본문) -> adapter/websocket/price
-> api/websocket/*
우연이었다. 어댑터를 리팩터링하면 경로가 끊기고 실시간이 죽는다.
그래서 vmkis/__init__.py 에 명시적 등록 import 를 넣어 고정하고,
새 인터프리터에서 import vmkis 만으로 레지스트리가 채워지는지
subprocess 로 격리 검증하는 테스트를 추가했다. 같은 프로세스 안에서는
다른 테스트가 모듈을 이미 적재해 거짓 통과가 나기 쉽다.
## 테스트 15개
test_client_websocket_does_not_import_api 는 AST 로 역방향 간선을
검사한다. #18 의 같은 발상이며 import-linter 도입 전까지의 경량
대체재다.
## 하위 호환
from vmkis.api.websocket import WEBSOCKET_RESPONSES_MAP 는 재export 로
그대로 동작한다. 기존 테스트 6곳의 monkeypatch.setitem 도 같은 dict
객체라 수정이 필요 없었다.
api/websocket/__init__.py 의 import 들은 부수효과가 목적이라 ruff 가
F401 로 잡았다. __all__ 에 넣어 의도를 드러냈다 — noqa 로 덮는 것보다
정직하다.
런타임 모듈레벨 역방향 11건 -> 10건.
ARCHITECTURE.md 의 "정리 대상" 2건이 모두 해소됐다.
Closes #17
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
파일 목록은 알파벳순으로 정렬된다. 하루에 여러 건을 쓰면 날짜만으로는 작성 순서를 알 수 없고 목록이 진행 순서와 무관하게 섞인다. 2026-08-28 에 10건을 쓴 결과가 이랬다. issue15_ issue17_ issue18_ issue19_20_ issue23_ issue25_ issue27_ issue2_ issue43_ label_ issue2_ 가 issue27 보다 뒤에 온다 — '_'(0x5F)가 '7'(0x37)보다 크기 때문이다. 실제 작성 순서는 issue2 가 첫 번째였다. YYYY-MM-DD_주제.md -> YYYY-MM-DD_nn_주제.md nn 은 그날의 작성 순번이며 두 자리로 고정한다. docs/prompts/ 에도 같은 규칙을 적용한다. 기존 파일은 그대로 둔다. 이름을 바꾸면 다른 문서의 링크가 깨지고, 기록물을 사후에 손대지 않는다는 원칙과도 어긋난다. 새로 쓰는 것부터 적용한다. 함께 고친 것: 참고 자료 절이 docs/reports/ARCHITECTURE_REPORT_V3_KR.md 를 가리켰는데 그 파일은 archive 로 옮겨져 없다. INDEX.md 와 ARCHITECTURE.md 로 교체했다. 상대 경로도 ../ 에서 ./ 로 고쳤다 (CLAUDE.md 는 저장소 루트에 있다). 마지막 갱신일이 2025-12-18 이었다. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md 의 "작업 완료 시" 절차 중 개발 일지와 To-Do List 를 세션
단위로 작성한다. 개별 작업 일지는 같은 날짜의 다른 파일에 있다.
새 명명 규칙(_nn_)을 처음 적용한 문서다. 기존 파일은 그대로 두므로
당분간 번호 있는 것과 없는 것이 섞이지만, 새 것부터 순서대로 쌓인다.
## 세션 결과
PyPI vm-stock-kis 0.0.1 (첫 배포)
PR 12건 머지 / 이슈 13건 종료
테스트 990 passed / TOTAL 90.83%
역방향 런타임 모듈레벨 12건 -> 10건
## 일지에 남긴 것
반복해서 드러난 네 가지를 정리했다.
1. 이름 스윕이 만든 결함을 세 번에 걸쳐 고쳤다. \bpykis\b -> vmkis 는
import 문에서는 옳지만 설치 명령과 "v2.x 시절 코드"에서는 틀리다.
특히 pip install vmkis 는 스윕이 "틀린 것을 그럴듯하게" 만든 사례다
2. 문서가 코드에 대해 사실이 아닌 것을 말하고 있었다. 암묵적 불변식을
처음 명문화했다
3. 테스트가 프로덕션 결함을 우회하고 있었다
4. 역방향 의존 2건을 같은 발상으로 없앴다 — 목록을 옮기는 대신
판단 근거를 당사자에게 넘긴다
검증 습관도 남겼다. 회귀 테스트를 넣은 뒤 버그를 일부러 되살려
실패하는지 확인하는 것이 두 번 값을 했고, #17 에서는 레지스트리 등록이
우연히 동작하는 것을 그 과정에서 발견했다.
## To-Do List
우선순위와 착수 전 확인 사항을 이슈별로 적었다. 블로커성 항목은
#43 하나다 — KisEndpoint 가 계좌 계열에만 적용된 중간 상태이고,
시세 계열은 Mock() 의 자동 속성 생성 때문에 테스트가 조용히 통과할
수 있어 78곳을 하나씩 확인해야 한다.
이슈로 등록하지 않은 미결 논의(real/virtual -> live/paper)도 근거와
함께 남겼다.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(api): 시세 계열을 선언적 엔드포인트 스펙으로 이관 (#43) domain="real" 을 손으로 붙이던 10곳을 KisEndpoint 로 옮깁니다. tr_virtual 을 생략하면 resolve() 가 모의 계좌에서도 실전 도메인을 돌려주므로 domain 인자 자체가 사라집니다. 시세 테스트가 TR ID 를 검증하지 않고 있었습니다. DOMESTIC_QUOTE.tr_real 을 WRONG_TR_ID 로 바꿔도 tests/unit/api/stock 165건이 전부 통과했습니다. test_endpoints.py 를 더해 되돌려 확인했습니다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth * fix(api): fetch() 에 없는 page 인자를 넘기던 2곳 — 표 2종 스펙 이관 (#43) 국내 일별 체결내역 조회와 국내 미체결 주문 조회가 self.fetch(..., page=page) 를 호출하고 있었습니다. fetch() 에는 page 인자가 없어 첫 호출에서 TypeError 로 죽습니다. 테스트가 잡지 못한 이유는 가짜 fetch 가 **kwargs 를 받았기 때문입니다 — 목은 시그니처를 검사하지 않습니다. 두 곳 모두 call(스펙, page=...) 로 이관하며 함께 고쳤습니다. 커서 길이 100 은 KIS 문서의 CTX_AREA_FK100 에서 확인했습니다. 남은 문자열 표 2종도 스펙으로 분해했습니다. 표를 전사하지 않고 런타임에 읽어 새 리터럴을 생성했으며, 쌍 완비(14/14 · 2/2)를 먼저 검증했습니다. FOREIGN_ORDER_MODIFY 는 키가 없으면 예외를 내는 동작을 유지합니다. test_call_contract.py 가 fetch/call 호출부를 AST 로 검사합니다. 결함을 되살려 실제로 실패하는지 확인했습니다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth * docs: 이슈 #43 시세 계열 이관 개발 일지 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
작업 목록이 저장소에 네 벌 있었고 서로를 몰랐습니다.
docs/reports/TODO_LIST_2025_12_17.md 2025-12-17, 명명 규칙 A
docs/reports/2026-08-28_TODO_LIST.md 2026-08-28, 명명 규칙 B
docs/generated/TODO_LIST.md 본문은 "2024년 12월"
docs/generated/todo.md poetry 시절
정렬 순서로는 2026-08-28_TODO_LIST.md 가 TODO_LIST_2025_12_17.md 앞에
옵니다('2' < 'T'). grep TODO_LIST 로 시작하면 8개월 낡은 문서를 먼저
만납니다.
가장 최근 것조차 고유 정보가 없었습니다. 116줄이 이슈 본문·이슈
코멘트·개발 일지·pyproject.toml 주석 어딘가에 전부 있었습니다. 게다가
"갱신하지 않는다"고 선언된 docs/reports/ 안에 아무도 켤 수 없는 체크박스
3개를 두고 있었습니다.
archive 규칙에 따라 본문은 고치지 않고 동결 안내만 붙였습니다.
이동으로 끊긴 링크 2곳을 복구했습니다.
Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: GitHub Discussions 폐지 — 창구를 이슈 하나로
8개월간 게시물이 GitHub 자동 생성 환영글 1건(댓글 0)뿐이었습니다. 제목이
"Welcome to python-kis Discussions!" — 포크 이전 이름이라, 외부 첫
방문자가 보는 첫 화면이 그것이었습니다.
설비가 있다는 전제 자체가 틀렸습니다.
- 템플릿 2종은 파일명이 카테고리 slug(q-a / ideas)와 달라 렌더링된 적이
없습니다 (question.yml / feature-request.yml)
- 템플릿 3종 모두 스키마가 무효입니다. required 가 validations: 가 아니라
attributes: 밑에 있습니다. 같은 저장소의 이슈 템플릿은 올바릅니다
git log 상 순서도 거꾸로입니다. 스위치를 먼저 켜고(07:48, 자동 환영글 생성)
7시간 뒤 가이드를 쓰고 9시간 뒤 템플릿을 커밋했습니다. 같은 커밋에 영상
스크립트와 PlantUML 이 들어 있습니다 — 필요해서가 아니라 "Phase 4 커뮤니티"
산출물 목록을 채운 것입니다.
문서 15곳이 운영되지 않는 창구를 안내하고 있었습니다. 정작 README 에는
안내가 없었습니다. #25(존재하지 않는 배포명) #29(포크 이전 절대경로)
#31(존재하지 않는 라벨) 과 같은 결함의 네 번째입니다.
저장소 설정에서 Discussions 를 비활성화했고(환영글 삭제 후), 템플릿 3종을
지웠으며, 588줄 설정 가이드는 archive/ 로 옮겼습니다. docs/reports/ 의
언급은 동결 기록물이므로 그대로 둡니다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth
* chore: 대기열 라벨 3종 도입 — next-up / blocked / needs-decision
폐지한 To-Do 마크다운이 하던 일 중 라벨로 옮길 수 있는 것을 옮깁니다.
next-up 다음에 착수할 것. 동시 최대 3건
blocked 선행 이슈 대기. 본문 첫 줄에 "선행: #NN" 필수
needs-decision 코드를 쓰기 전에 하나 골라야 하는 것
라벨이 무너지는 지점은 붙일 때가 아니라 뗄 때입니다. 지적할 리뷰어가
없으므로 PR 템플릿 체크리스트로 대신합니다.
마일스톤은 도입하지 않습니다. #30 이 #33·#34·#35·#36 을 GitHub 네이티브
서브이슈로 물고 있어 진행률이 자동 계산되므로, 1.0.0 묶음의 완료 판정에
추가 설비가 필요하지 않습니다. 다만 서브이슈는 포함 관계이지 순서 의존이
아니므로 blocked 라벨은 별도로 필요합니다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md 전문 269줄에 "이슈", "GitHub", "PR", "라벨" 이 한 번도 나오지 않았습니다. 새 세션의 AI 는 이 문서만 읽으면 작업 상태가 마크다운에 있다고 결론짓습니다. 실제로 그렇게 해서 116줄짜리 복제본이 생겼습니다. 삭제한 것 - "To-Do List 작성" 3곳 (108 / 222 / 254행) - "Phase별 문서 요구사항" 절 전체 Phase 폐기의 근거는 실측했습니다. 2026 이후 커밋 47건 중 Phase 표기 0건인데, "Phase 완료 시 완료 보고서" 규칙이 만든 산출물 4건은 동결된 채 남아 있습니다. 신설한 것 - "작업 상태는 어디에 사는가" — 무엇이 어디에 사는지 9행 표 - "세션 시작 시" — 대기열 조회 착수 전에 문서가 자기 규칙을 어기고 있는 것을 발견했습니다. 문서 체계 트리 바로 위에 "실제 존재하는 파일만 적습니다"라고 적어 놓고 존재하지 않는 경로 4개를 가리켰습니다 (ARCHITECTURE_REPORT_V3_KR.md, DEVELOPMENT_REPORT_*.md, user/QUICKSTART.md, user/TUTORIALS.md). #25 #29 #31 과 같은 결함이, 그 결함을 경고하는 문서 안에 있었습니다. AGENT_WORKFLOW_RULES.md 의 사실 오류 2건도 고쳤습니다. - "apply_patch 로 편집" — git grep 결과 이 문서에만 등장하는 도구 - "reports/coverage_html" — 실제 경로는 reports/htmlcov/ "매 프롬프트마다 프롬프트 문서 작성"은 지켜진 적이 없습니다. 2026-08-28 에 개발 일지 12건이 쌓이는 동안 프롬프트 문서는 5건이었습니다. 실제 운영에 맞춰 "작업을 시작하는 요청 하나당 한 건"으로 바꿨습니다. 문서에 적은 gh 명령은 전부 실행해 보고 넣었습니다. 검증 안 된 명령을 규칙 문서에 넣는 것이 이 개정이 고치려는 실패 양상 그 자체입니다. Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
같은 모양의 while True 루프가 8곳에 복사돼 있었고, 다른 것은 "어느 필드에 누적하는가" 한 줄뿐이었습니다. api/account/ 순 -104줄. 이슈는 루프 11곳을 한 종류로 봤지만 두 계열이었습니다. KIS 커서 연속조회 8곳 KisPage + is_last + next_page. 골격 동일 날짜/시간 커서 3곳 KisPage 를 아예 안 씀. 봉 시각에서 커서를 도출 차트 계열은 종료 조건이 이질적이고(빈 봉 / cursor < last / start 가 timedelta 일 때 재계산 / 해외는 시각별 dedup), 해외 당일차트는 for i in range(FOREIGN_MAX_PERIODS) 로 NMIN 을 늘리는 다른 기제입니다. 억지로 밀어 넣으면 역효과이므로 8곳만 덮습니다. 설계는 merge 콜백을 골랐습니다. 이슈의 "제외" 항목이 응답 클래스 변경을 배제했고, __merge__ 는 누적 규칙이 루프에서 멀어지며, 문자열 필드명은 타입 검사를 잃습니다. 밟은 함정: response_type 은 팩토리여야 합니다. dynamic.py:257 이 인스턴스를 받으면 그 인스턴스에 그대로 파싱하므로, 하나를 돌려 쓰면 모든 페이지가 같은 객체가 되고 merge 가 자기 자신을 이어붙여 결과가 조용히 불어납니다. 예전 루프들이 매 반복 응답 객체를 새로 만든 이유입니다. 인스턴스를 주면 TypeError 로 막습니다. 페이징 루프를 직접 검증하는 테스트가 하나도 없었습니다. 신설한 9건에 대해 네 가지 변이(continuous 무시 / is_last 무시 / 첫 페이지 continuous / merge 생략)를 만들어 전부 실패하는지 확인했습니다. order_profit.py 의 fetch 2곳은 #43 대상 목록에 없었지만 페이징 이관에 스펙이 필요해 함께 만들었습니다. Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
614b68e 가 "타이밍 단언의 상한 여유 확대"로 상한 6곳을 SCHEDULING_SLACK 으로 바꾸면서 이 한 곳을 빠뜨렸습니다. 하필 스레드 4개를 동시에 돌려 스케줄링에 가장 민감한 테스트인데 여유가 가장 좁았습니다(기대 1.0 에 +0.3). 커버리지를 켜면 10회 중 1회꼴로 터졌습니다. 하한 0.9 는 그대로 둡니다. 하한은 "유량 제한이 실제로 걸렸는가"를 검증하므로 엄격해야 합니다. 상한만 늘리고 통과만 보면 회귀를 못 잡는 상태가 될 수 있어, 프로덕션 코드를 변이시켜 두 경계를 각각 확인했습니다. 대기 sleep 제거 -> assert 0.9 <= 0.0009… 하한이 잡음 대기에 period*3 추가 -> assert 4.05… <= (1.0 + 2.0) 상한이 잡음 수정 후 커버리지 ON 20회 연속 통과. Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
이슈 2건(#43 #44)을 닫고 작업 관리 방식을 마크다운에서 이슈 트래커로 옮겼습니다. PR 6건 머지. 새 규칙에 따라 To-Do List 마크다운은 만들지 않습니다. 다음 세션이 볼 곳은 gh issue list --label next-up 입니다. Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: import-linter 계약으로 역방향 의존 회귀 차단 (#50) ARCHITECTURE.md 불변식 2번("새로운 모듈-레벨 역방향 간선을 만들지 않습니다")을 기계화합니다. #17 · #18 이 없앤 간선 2건이 되살아나는 것을 CI 가 막습니다. 계약을 넣으며 세 가지가 드러났습니다. 1. grimp 이 패키지의 1/4 만 보고 있었습니다. src/vmkis 의 디렉터리 18개 중 13개에 __init__.py 가 없어(암묵적 네임스페이스 패키지) root_package 단수로는 모듈 20개만 잡힙니다. root_packages 복수로 나열해 92개 전부를 담습니다. 빠뜨려도 조용히 초록이 되므로 그래프 커버리지 가드 테스트를 함께 넣습니다. 2. 세 번째 역방향 간선이 있었습니다. utils/diagnosis.py 의 모듈 레벨 `import vmkis` 가 루트 파사드를 통해 kis/api/client/scope 전체를 끌어옵니다. 필요한 값은 버전과 배포명 둘뿐이라 vmkis.__env__ 를 직접 봅니다. 3. ignore_imports 는 위치를 보지 않습니다. 면제된 지연 import 를 모듈 레벨로 올려도 계약은 통과합니다(실측). AST 테스트가 그 자리를 막습니다. 그 지연 import 에 없던 사유 주석도 달았습니다(불변식 3번). 되돌려 확인: 위반 5종을 일부러 만들어 3종이 계약에, 2종이 테스트에 잡히는 것을 확인했습니다. 상세는 개발 일지에 있습니다. Closes #50 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq * docs: 남은 미결을 #63 · #64 로 분리 (#50) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#38 이 VmKis.close() 에 getattr 가드를 넣어 근본 원인을 고쳤으므로 그것을 우회하던 @patch("vmkis.kis.VmKis.__del__", ...) 3곳을 지웁니다. 지우기만 해서는 이슈의 목적("회귀 탐지력 회복")이 절반만 달성됩니다. 실측: 패치를 지운 뒤 close() 의 getattr 가드를 걷어내고 돌리면 42 passed, 3 warnings 입니다. 회귀가 로그에 보이지만 CI 를 막지 않습니다. 그래서 이슈가 "(선택)"으로 남긴 filterwarnings 를 함께 넣습니다. filterwarnings = ["error::pytest.PytestUnraisableExceptionWarning"] 같은 조작이 이제 패치가 붙어 있던 바로 그 3건을 실패시킵니다. 전역 filterwarnings = ["error"] 는 쓰지 않습니다. 스위트에 남은 무해한 경고 9건까지 전부 실패로 만듭니다. close() 의 가드에는 무엇이 자기를 지키는지 역참조 주석을 달았습니다. 가드 옆에 그것이 없으면 다음 사람은 "불필요한 방어"로 읽습니다. Closes #42 Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* build: 서브패키지 13곳에 __init__.py 추가 (#64) + event→api 판정 (#63) ## #64 — A 채택 src/vmkis 의 디렉터리 18개 중 13개에 __init__.py 가 없었습니다. 암묵적 네임스페이스 패키지라 grimp 이 스캔에서 건너뛰고, 루트 하나만 주면 모듈 92개 중 20개만 잡힌 채 계약이 초록으로 통과했습니다(#50). 설계가 아니었음을 먼저 확인했습니다. git 이력상 삭제된 적이 없고(업스트림 원본), 문서에 namespace 언급 0건, pkgutil/walk_packages 사용처 0건입니다. 측정 결과 A 는 손해가 없습니다. 그래프 모듈 20 -> 92 설정 root_packages 6줄 -> root_package 1줄 테스트 1052 passed 유지 계약 2 kept, 0 broken 유지 고의 위반 2종 여전히 잡힘 휠 80 -> 93 파일 13개 전부 주석만 넣고 비워 둡니다. 재export 허브가 되면 그것이 순환의 시작이고, __init__.py 를 만든다는 것은 "편의 import 를 넣고 싶은 자리" 13개를 만드는 일이기도 합니다. 각 파일이 스스로 그러지 말라고 말합니다. ## #63 — 의도적 (코드 변경 없음) event -> api 3건은 전부 어노테이션 전용이라 TYPE_CHECKING 으로 옮길 수 있습니다. 실제로 옮겨 보고 되돌렸습니다. 1. api <-> event 는 양방향 순환입니다(12 <-> 4). event -> api 만 없애도 순환은 남습니다. 이미 의도적으로 동결한 api <-> adapter(6 <-> 32)와 구조가 같습니다. 2. 옮기면 get_type_hints(KisSimpleProduct/KisSimpleOrderNumber) 가 동작하던 것이 NameError 가 됩니다. isinstance 는 계속 되므로 테스트 1052건은 전부 통과합니다 - 테스트로는 이 손실이 안 보입니다. 불변식 2번 동결 표에 행을 추가하고, 판정을 뒤집을 유일한 조건(MARKET_TYPE 의 자리)을 불변식 4번에 적었습니다. Closes #63 Closes #64 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq * docs: 로컬 428 vs CI 359 수치 불일치의 원인 정정 (#64) 앞 커밋의 개발 일지와 PR 본문이 "root_packages 로 겹치는 루트를 주면 중복 계상된다"고 적었습니다. 틀렸습니다. 간선 집합을 덤프해 비교하니 작업트리와 깨끗한 클론이 470개로 완전히 동일합니다. 차이는 그래프가 아니라 .grimp_cache 였습니다. lint-imports --no-cache 작업트리 428 / 클론 428 ← 일치 grimp 의 캐시는 파일 단위로만 무효화되고 세션 설정 변경(root_packages 복수 -> root_package 단수)은 무효화하지 않습니다. 옛 설정으로 만든 캐시가 재사용됐습니다. 계약 판정 자체는 오염되지 않습니다 - 따뜻한 캐시에서 고의 위반을 넣어 "1 kept, 1 broken" 을 확인했습니다. 틀어지는 것은 보고되는 수치뿐입니다. .grimp_cache 는 grimp 이 스스로 .gitignore(*) 를 써 넣으므로 저장소에 들어가지 않습니다. pyproject.toml 의 계약 설정 옆에 --no-cache 안내를 남깁니다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
requires_api 17개가 전부 tests/unit/ 에 있었습니다. 실제 KIS 서버에 HTTP 요청을
보내고 실계좌 자격증명이 필요한 테스트입니다. 디렉터리와 마커가 서로 다른 말을
하면 "tests/unit/ 을 돌린다"의 의미가 깨집니다.
갈라 옮길 필요는 없었습니다. 두 파일 다 pytestmark 로 파일 전체가 requires_api
입니다(6/6, 11/11). git mv 두 번으로 끝났고 from tests.env import 도 새 위치에서
그대로 동작합니다.
## "(선택)" 항목이 선택이 아니었습니다
이슈가 괄호로 남긴 디렉터리 단위 마커를 재 보니 이미 어긋나 있었습니다.
tests/integration 전체 29개 수집 / integration 마커 9개
tests/performance/conftest.py 가 만들어진 이유(30개 중 8개)와 같은 상황이 같은
저장소에서 두 번째로 반복된 것입니다. 게다가 이 이동 자체가 마커 없는 파일을
5개에서 7개로 늘릴 참이었습니다. tests/integration/conftest.py 를 함께 넣습니다.
게이팅은 바뀌지 않습니다. CI 게이트는 -m 'not requires_api and not performance'
라 integration 을 제외하지 않습니다.
되돌려 확인: 마커 없는 새 파일을 넣어 conftest 유무로 1 / 0 을 확인했고, 저장소
전체 -m integration 이 46개(29+17), tests/unit 은 0개임을 확인했습니다.
## 함께 고친 것
CONTRIBUTING.md 의 테스트 구조 트리가 없는 경로 4개를 가리키고 있었습니다
(tests/fixtures/, test_stock_quote.py, test_websocket.py, test_load_config.py).
CLAUDE.md 가 자기 트리에 대해 적어 둔 것과 같은 문제라 ls 해서 다시 썼습니다.
docs/developer/DEVELOPER_GUIDE.md 의 트리는 6개 전부 허구지만, 그 문서는 트리만
틀린 게 아니라 전체가 옛 레이아웃 기준이라 범위 밖으로 남깁니다.
Closes #41
Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.claude/` 를 통째로 무시하면 성격이 반대인 두 파일이 같이 묻힙니다.
settings.json git mv / gh pr / git diff — 머신 독립적, 공유 대상
settings.local.json 절대경로 + PowerShell 하드코딩 — 다른 머신에서 무의미
앞으로 `.claude/agents/`, `.claude/commands/` 를 만들어도 같이 사라질
참이었습니다. CLAUDE.md 를 이만큼 관리하면서 AI 작업 규칙의 나머지 절반을
추적하지 않는 것은 앞뒤가 맞지 않습니다.
추적으로 돌리면서 다시 매칭될 일이 없는 일회성 허용 규칙 2건을 뺐습니다.
- 저장소 이름 변경 때 한 번 쓴 `xargs -0 sed -i ...QuantumOmega...`
- `gh issue create --title 'fix(tests): 벤치마크 테스트가 시계 해상도...'`
— 이슈 하나를 만들기 위한 제목 문자열이 통째로 영구 권한 규칙이었습니다.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#55 는 needs-decision 이슈였고 산출물은 결정입니다. 코드는 바꾸지 않았습니다. 결정: 변경 (live/paper). 근거는 `real` 이 벤더 표기가 아니라는 것과, 사용자가 사실상 0명인 지금이 가장 싸다는 것입니다. 착수 전 실측에서 세 가지가 나왔고 전부 후속 이슈의 범위를 바꿨습니다. - 이슈가 든 근거 하나가 무너졌습니다. `Realtime` 개수(219/278)는 정확했지만 충돌은 부분일치에서만 생깁니다 — 단어 경계로 재면 46건 중 realtime 은 1건. 개수가 맞다고 그 개수가 뒷받침한다는 문장까지 맞는 것은 아닙니다. - 이슈가 "가장 중요한 한 줄"로 꼽은 위험이 개명 이후가 아니라 **이미** 열려 있었습니다. `cfg.get("virtual", False)` 는 기본값이 실전이라, 오타 `virtaul: true` 가 지금도 조용히 실전 계좌로 붙습니다. - 그 읽기 코드가 5벌 복붙돼 있고 4벌이 examples/ 입니다. helpers.py 만 고치면 사용자가 복사해 가는 쪽은 그대로입니다. 그래서 순서를 뒤집었습니다. 개명은 곧 "옛 키"를 만드는 행위라 가드 없이 개명하면 개명 자체가 사고의 원인이 됩니다. #69 load_config 5벌 통합 + 미지의 키에 명시적 실패 (next-up) #70 real/virtual → live/paper 개명 (blocked, 선행 #69) #70 에는 YAML 스키마 변경도 넣었습니다. 프로필 이름과 플래그가 같은 사실을 두 번 적고 어긋났을 때의 정의가 없어서, 불리언 `virtual: true/false` 를 `mode: live|paper` enum 으로 바꿉니다. 불리언은 "없음"이 곧 False(실전)지만 enum 은 "없음"이 그냥 없음입니다. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cfg.get("virtual", False)` 는 기본값이 실전이었습니다. `virtaul: true` 오타 하나로
모의투자 의도가 실전 주문이 됐고, 경고 한 줄 없었습니다.
설정 파일이 말하는 것 : virtaul(오타) = True -> 사용자 의도: 모의투자
create_client 가 볼 값: virtual = False -> 실전 계좌
같은 코드가 5벌이었고 4벌이 examples/ 였습니다. 예제는 사용자가 그대로 복사해
가는 코드라 helpers.py 만 고쳐서는 막히지 않습니다.
- `load_config` 를 1벌로. 예제 4개는 `from vmkis import load_config`
- `_validate_profile` 신설 — 모르는 키 / 필수 키 누락 / 판정 키 누락에 예외
- `create_client` 에서 `.get(..., False)` 제거. 기본값을 두지 않습니다
- 읽기와 쓰기가 `_MODE_KEY` 상수 하나를 공유. 한쪽만 바뀌어 어긋나던 것을 막습니다
- `load_config` 를 패키지 루트에 공개 (추가이므로 하위호환 유지)
## 테스트가 그 위험을 사양으로 못 박고 있었습니다
tests/unit/test_helpers.py:133
def test_virtual_key_defaults_to_false(...):
"""`virtual` 키가 없으면 실전으로 간주한다."""
막으려던 동작이 통과해야 할 사양으로 적혀 있었습니다. 뒤집었습니다.
`examples/01_basic/get_quote.py` 의 복사본을 importlib 로 끌어와 테스트하던
파일도 있었습니다 — 중복을 테스트가 고착시키고 있었습니다. 배포되는
config.example*.yaml 3개를 파싱하는 값어치는 남기고 대상만 라이브러리로 옮겨
test_config_examples.py 로 바꿨습니다.
## 되돌려 확인
`_validate_profile` 무력화 + `.get(..., False)` 복원 상태에서 7건이 실패하고,
복원 해제 후 1058건이 통과하는 것을 확인했습니다.
pytest -m 'not requires_api' 1058 passed, 8 skipped
coverage 91.58% (게이트 90), helpers.py 100%
ruff / lint-imports 통과
Closes #69
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(config): 설정 스키마 문서 신설 + #70 을 코드 개명으로 축소 사용자가 config.example*.yaml 3개를 스키마 전면 재설계로 고쳤습니다. 그것은 #70 이 아닙니다 — #70 은 코드 식별자 개명이고, 새 스키마는 라이브러리에 대응 개념이 없는 새 설정 계층입니다. base_url / ws_url __env__.py:10-14 하드코딩 상수. 주입 지점 없음 apps / accounts KisAuth 는 필드 5개. 앱 개념 없음 broker 브로커 개념 자체가 없음 token_path 기본 ~/.vmkis/ + 해시 파일명 그래서 쪼갰습니다. 코드 개명은 어떤 설정 설계에서도 살아남지만, 스키마는 설계 확정이 필요합니다. 묶으면 스키마가 막힐 때마다 개명까지 멈춥니다. ## 새 스키마 — 3블록 운용 시스템 쪽 설정을 참고했으나 대부분 옮기지 않았습니다. 자금 배분 정책, 원장 표시용 이름표, 레거시 브리지용 이중 명명, 다중 브로커는 전부 이 라이브러리가 읽지 않는 값입니다. 이 저장소는 KIS 전용 API 클라이언트이지 운용 시스템이 아닙니다. version / apps / accounts / default_account ## 초안 결함 5건을 반영했습니다 - config.example.real.yaml 의 default_account 가 그 파일에 없는 계좌를 가리킴 -> R5/R6 양방향 참조 검사 - VMKIS_PROFILE 이 가리킬 대상이 없음 -> 선택 축을 default_account 로 일원화 - token_path 기준 경로 미정의 -> 설정 파일 기준, 파일명은 앱 이름에서 파생 - user_agent 따옴표 이중 -> 필드 제거 - accounts 가 스칼라 키와 블록 혼재 -> default_account 를 최상위로 token_path 를 파생시킨 것이 설계 변경입니다. 초안은 "앱키별로 다르게 지정해야 한다"고 경고했는데, 사용자가 지켜야 하는 불변식은 사용자가 안 지킵니다. 두 앱이 같은 경로를 가리켜도 아무도 못 막고 증상은 "가끔 인증이 풀린다"로 나타납니다. 하위 호환은 넣지 않습니다(사용자 결정). 다만 옛 파일은 version 키가 없어 R1 에서 읽을 수 있는 메시지로 거부됩니다 — 조용히 오독되지 않습니다. 사용자 초안은 draft/config-schema-v2 에 보존했습니다. #70 이 virtual 을 369곳 건드리므로 확정되지 않은 스키마를 작업 트리에 두지 않습니다. Refs #70, #75 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(config): user_agent·endpoints 되살리고 따옴표 규칙(R9) 추가 검토 중 세 건이 나왔습니다. 둘은 제가 잘못 잘라낸 것입니다. ## user_agent — 최상위로 되살림 초안의 broker_env_kis.etc.user_agent 를 블록째 지웠는데, 항목의 값어치를 따로 재지 않은 실수였습니다. 이미 있는 손잡이인데 하드코딩돼 있습니다 — __env__.py 의 USER_AGENT 가 kis.py 에서 세션 헤더에 들어갑니다. 초안 주석이 브라우저 UA 복사를 안내한 것은 실제 필요로 읽힙니다. 브로커 블록이 아니라 최상위입니다. 클라이언트 전체에 걸리는 값입니다. ## endpoints — 추가 "스테이징 서버가 없으니 쓸 사람이 없다"는 판단을 정정합니다. 사용 사례를 잘못 상정했습니다. 진짜 사례는 벤더가 주소를 바꿨을 때의 자력 복구입니다. 지금은 사용자가 손을 쓸 수 없습니다. from-import 가 값을 복사하므로 __env__ 를 고쳐도 소비 모듈은 옛 값을 봅니다. $ python -c "import vmkis.__env__ as env, vmkis.kis as k; \ env.REAL_DOMAIN='https://patched.example.com'; print(k.REAL_DOMAIN)" https://openapi.koreainvestment.com:9443 모듈마다 따로 패치해야 하는데 문서에 없고, 새 모듈이 그 상수를 import 하면 또 깨집니다. 남는 수단은 릴리스를 기다리는 것뿐이고 장중이면 그날은 끝입니다. mode 로 키를 잡고 부분 지정을 허용합니다 — 벤더가 웹소켓 포트만 바꾸는 일이 흔합니다. 소비 지점 2곳이 이미 객체를 들고 있어 새 배선이 필요 없습니다. ## R9 — 따옴표 함정 account_no: 00000000 -> 0 (int) 계좌번호가 사라집니다 product_code: 01 -> 1 (int) mode: paper -> 'paper' (str) 이건 안전합니다 mode 는 따옴표 유무가 같은 결과라 문서에서 빼고 적고 있었는데, 그걸 본 사용자가 "따옴표는 선택"으로 읽으면 account_no 에서 값이 조용히 0 이 됩니다. 안전한 값 하나를 따옴표 없이 적는 대가로 위험한 값에서 따옴표가 빠집니다. 예시를 전부 따옴표로 통일하고(version 만 예외 — 실제로 정수), 문자열 자리에 int/bool 이 오면 따옴표를 씌우라고 말해주며 거부하는 R9 을 넣었습니다. Refs #75 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#76 에서 docs/guidelines/CONFIG_SCHEMA.md 를 추가하면서 INDEX.md 갱신을 빠뜨렸습니다. INDEX 는 guidelines 를 개별 나열하는 표라, 파일만 늘고 목록이 그대로면 새 문서를 찾을 방법이 없습니다. 양방향으로 대조해 다른 누락·죽은 링크가 없는 것을 확인했습니다. INDEX 에만 있음 (죽은 링크): 없음 파일만 있음 (미등재) : 없음 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
설정 파일을 새 스키마로 갈아엎습니다. 하위 호환은 없습니다.
version / apps / accounts / default_account
apps 를 계좌와 분리하는 근거는 토큰 수명 하나입니다. KIS 토큰은 app_key 단위로
발급되므로 같은 앱키를 쓰는 계좌 N개가 토큰 1개를 공유합니다. 그것이 KIS 의 실제
제약이라 라이브러리가 알아야 합니다.
## 초안에서 고친 것
- default_account 가 그 파일에 없는 계좌를 가리킴 -> R5/R6 양방향 참조 검사
- token_path 기준 경로 미정의 -> 설정 파일 기준, 앱 이름에서 파생
- accounts 가 스칼라 키와 블록 혼재 -> default_account 를 최상위로
- user_agent 따옴표 이중 -> 값 한 겹으로
token_path 를 파생시킨 것이 설계 변경입니다. 초안은 "앱키별로 다르게 지정해야
한다"고 경고했는데, 사용자가 지켜야 하는 불변식은 사용자가 안 지킵니다. 두 앱이
같은 파일을 가리켜도 아무도 못 막고 증상은 "가끔 인증이 풀린다"로 나타납니다.
## R9 — YAML 의 함정
account_no: 00000000 -> 0 (int)
product_code: 01 -> 1 (int)
결함을 되살린 상태에서 KisAuth 가 받는 계좌가 '0-1' 이 되는 것을 확인했습니다.
사용자의 오타가 아니라 형식의 함정이라, 오류 메시지가 따옴표를 씌우라고 말합니다.
## 템플릿 위치
configs/ 안에 둡니다. 토큰 폴더가 설정 파일 기준이라, 템플릿이 저장소 루트에
있으면 제자리에서 채웠을 때 토큰이 루트에 떨어지고 그건 .gitignore 에 없습니다.
.gitignore 가 `configs/` 가 아니라 `configs/*` 인 이유도 실측했습니다 —
디렉터리째 제외하면 git 이 안으로 내려가지 않아 `!` 예외가 통하지 않습니다.
## user_agent / endpoints 배선
파싱만 하고 안 읽는 키를 내보내지 않기 위해 VmKis 까지 배선했습니다. endpoints 는
벤더가 주소를 바꿨을 때의 탈출구입니다 — from-import 가 값을 복사하므로 사용자가
__env__ 를 고쳐도 소용이 없고, 지금은 릴리스를 기다리는 수밖에 없습니다.
## 함께 고친 것
영문 문서가 한 번도 맞은 적이 없는 API 를 적고 있었습니다. load_config 가
{'kis': ...} 를 준 적이 없는데 VmKis(**config['kis']) 라고 적혀 있었고,
VmKis(app_key=, app_secret=, account_number=, server=) 는 네 인자 모두
존재하지 않습니다. 설정에 직결된 곳만 정정하고 나머지는 #78 로 남겼습니다.
## 되돌려 확인
R2·R6·R9 무력화 시 7건 실패, 복원 후 전체 통과를 확인했습니다.
pytest -m 'not requires_api' 1088 passed, 8 skipped
coverage 91.79% (게이트 90), config.py 100%
ruff / lint-imports 통과
Closes #75
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
🛠️ PR Summary
🌟 요약
어떤 것이 변경되었나요? 간략히 설명해주세요.
인증 토큰을 자동으로 관리하는 기능을 추가했습니다.
📊 주요 변경 사항
주요 변경 사항을 적어주세요.
utils.workspace.py파일을 추가했습니다.PyKis 라이브러리의 개인 작업 공간을 관리하는 기능을 추가했습니다.
kis.py에서PyKis메인 클래스 생성자에 keep_token 인자를 추가했습니다.keep_token이 True이면 인증 토큰을 개인 작업 공간에서 자동으로 관리합니다.
🎯 목적 및 영향
목적: 왜 이 PR이 필요한가요?
인증 토큰을 자동으로 관리하는 기능을 추가하여 라이브러리를 손쉽게 사용할 수 있도록 합니다.
영향: 이 변경 사항이 어떤 영향을 미치나요?
토큰 로드 및 저장을 자동으로 처리하므로 비전문 사용자가 토큰을 관리하는 부담이 줄어듭니다.