From ac09c4db38139cdd6b4616a1857bb9f3bb9dab4a Mon Sep 17 00:00:00 2001 From: Soju06 Date: Tue, 10 Dec 2024 14:45:46 +0900 Subject: [PATCH 1/4] chore: use functools cached_property --- pykis/api/account/balance.py | 10 ++++++---- pykis/api/account/daily_order.py | 10 ++++++---- pykis/api/account/order_profit.py | 13 +++++++------ pykis/api/account/orderable_amount.py | 7 +++---- pykis/api/stock/quote.py | 22 ++++++++++------------ pykis/utils/cache.py | 26 -------------------------- 6 files changed, 32 insertions(+), 56 deletions(-) delete mode 100644 pykis/utils/cache.py diff --git a/pykis/api/account/balance.py b/pykis/api/account/balance.py index 97706c32..211103c5 100644 --- a/pykis/api/account/balance.py +++ b/pykis/api/account/balance.py @@ -1,4 +1,5 @@ from decimal import Decimal +from functools import cached_property from typing import TYPE_CHECKING, Iterator, Protocol, runtime_checkable from pykis.adapter.account_product.order import ( @@ -27,7 +28,6 @@ from pykis.responses.dynamic import KisDynamic, KisList, KisObject, KisTransform from pykis.responses.response import KisAPIResponse, KisPaginationAPIResponse from pykis.responses.types import KisAny, KisDecimal, KisString -from pykis.utils.cache import cached from pykis.utils.repr import kis_repr from pykis.utils.typing import Checkable @@ -748,12 +748,14 @@ class KisForeignBalanceStock(KisDynamic, KisBalanceStockBase): purchase_amount: Decimal = KisDecimal["frcr_pchs_amt1"] """매입금액""" - @property - @cached - def exchange_rate(self) -> Decimal: + # Pylance bug: cached_property[Decimal] type inference error. + @cached_property + def exchange_rate(self) -> Decimal: # type: ignore """환율 (캐시됨)""" return self.balance.deposits[self.currency].exchange_rate + exchange_rate: Decimal + class KisForeignBalance(KisPaginationAPIResponse, KisBalanceBase): """한국투자증권 해외종목 잔고""" diff --git a/pykis/api/account/daily_order.py b/pykis/api/account/daily_order.py index 9d308742..8edf9f96 100644 --- a/pykis/api/account/daily_order.py +++ b/pykis/api/account/daily_order.py @@ -1,5 +1,6 @@ from datetime import date, datetime, timedelta from decimal import Decimal +from functools import cached_property from typing import TYPE_CHECKING, Any, Iterable, Protocol, runtime_checkable from zoneinfo import ZoneInfo @@ -29,7 +30,6 @@ from pykis.responses.dynamic import KisDynamic, KisList, KisTransform from pykis.responses.response import KisPaginationAPIResponse from pykis.responses.types import KisAny, KisDecimal, KisString -from pykis.utils.cache import cached from pykis.utils.repr import kis_repr from pykis.utils.timezone import TIMEZONE @@ -490,9 +490,9 @@ class KisForeignDailyOrder(KisDynamic, KisDailyOrderBase): number: str = KisString["odno"] """주문번호""" - @property - @cached - def order_number(self) -> KisOrder: + # Pylance bug: cached_property[KisOrder] type inference error. + @cached_property + def order_number(self) -> KisOrder: # type: ignore """주문번호""" return KisSimpleOrder.from_order( account_number=self.account_number, @@ -504,6 +504,8 @@ def order_number(self) -> KisOrder: kis=self.kis, ) + order_number: KisOrder + name: str = KisString["prdt_name"] """종목명""" diff --git a/pykis/api/account/order_profit.py b/pykis/api/account/order_profit.py index aa338b8d..d7a40838 100644 --- a/pykis/api/account/order_profit.py +++ b/pykis/api/account/order_profit.py @@ -1,5 +1,6 @@ from datetime import date, datetime from decimal import Decimal +from functools import cached_property from typing import TYPE_CHECKING, Iterable, Protocol, runtime_checkable from zoneinfo import ZoneInfo @@ -15,14 +16,12 @@ KisMarketType, get_market_code, get_market_code_timezone, - get_market_timezone, ) from pykis.client.account import KisAccountNumber from pykis.client.page import KisPage from pykis.responses.dynamic import KisDynamic, KisList, KisTransform from pykis.responses.response import KisPaginationAPIResponse -from pykis.responses.types import KisAny, KisDecimal, KisInt, KisString -from pykis.utils.cache import cached +from pykis.responses.types import KisAny, KisDecimal, KisString from pykis.utils.repr import kis_repr from pykis.utils.timezone import TIMEZONE @@ -438,9 +437,9 @@ class KisForeignOrderProfits(KisPaginationAPIResponse, KisOrderProfitsBase): _end: date _country: COUNTRY_TYPE | None = None - @property - @cached - def fees(self) -> Decimal: + # Pylance bug: cached_property[Decimal] type inference error. + @cached_property + def fees(self) -> Decimal: # type: ignore """ 수수료 조회 (모의투자 미지원) @@ -454,6 +453,8 @@ def fees(self) -> Decimal: country=self._country, ) + fees: Decimal + def __init__( self, account_number: KisAccountNumber, diff --git a/pykis/api/account/orderable_amount.py b/pykis/api/account/orderable_amount.py index f2b1440c..693445f4 100644 --- a/pykis/api/account/orderable_amount.py +++ b/pykis/api/account/orderable_amount.py @@ -1,4 +1,5 @@ from decimal import Decimal +from functools import cached_property from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from pykis.api.account.order import ( @@ -22,8 +23,7 @@ KisResponseProtocol, raise_not_found, ) -from pykis.responses.types import KisDecimal, KisInt -from pykis.utils.cache import cached +from pykis.responses.types import KisDecimal from pykis.utils.repr import kis_repr if TYPE_CHECKING: @@ -191,8 +191,7 @@ class KisDomesticOrderableAmount(KisAPIResponse, KisOrderableAmountBase): foreign_only_amount: Decimal = KisDecimal["ord_psbl_frcr_amt_wcrc"] """외화주문가능금액 (원화환산)""" - @property - @cached + @cached_property def _foreign(self) -> "KisDomesticOrderableAmount": """ 한국투자증권 국내 주식 주문가능금액 조회 diff --git a/pykis/api/stock/quote.py b/pykis/api/stock/quote.py index 78bd712d..379fcd97 100644 --- a/pykis/api/stock/quote.py +++ b/pykis/api/stock/quote.py @@ -1,5 +1,6 @@ from datetime import date from decimal import Decimal +from functools import cached_property from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable from pykis.api.base.product import KisProductBase, KisProductProtocol @@ -22,7 +23,6 @@ KisInt, KisString, ) -from pykis.utils.cache import cached, set_cache from pykis.utils.repr import kis_repr from pykis.utils.timezone import TIMEZONE @@ -552,9 +552,9 @@ def change(self) -> Decimal: """전일대비""" return self.price - self.prev_price - @property - @cached - def indicator(self) -> KisForeignIndicator: + # Pylance bug: cached_property[KisForeignIndicator] type inference error. + @cached_property + def indicator(self) -> KisForeignIndicator: # type: ignore """종목 지표""" return foreign_quote( self.kis, @@ -563,6 +563,8 @@ def indicator(self) -> KisForeignIndicator: extended=False, ).indicator + indicator: KisForeignIndicator + open: Decimal = KisDecimal["open"] """당일시가""" high: Decimal = KisDecimal["high"] @@ -605,14 +607,10 @@ def __pre_init__(self, data: dict): super().__pre_init__(data) if not self.extended: - set_cache( - self, - "indicator", - KisObject.transform_( - data["output"], - KisForeignIndicator, - ignore_missing=True, - ), + self.indicator = KisObject.transform_( + data["output"], + KisForeignIndicator, + ignore_missing=True, ) diff --git a/pykis/utils/cache.py b/pykis/utils/cache.py deleted file mode 100644 index 9c448270..00000000 --- a/pykis/utils/cache.py +++ /dev/null @@ -1,26 +0,0 @@ -__all__ = [ - "cached", - "set_cache", - "get_cache", -] - - -def cached(fn): - def wrapper(*args, **kwargs): - self = args[0] - cache_key = f"__{fn.__name__}" - - if not hasattr(self, cache_key): - setattr(self, cache_key, fn(*args, **kwargs)) - - return getattr(self, cache_key) - - return wrapper - - -def set_cache(obj: object, key: str, value: object): - setattr(obj, f"__{key}", value) - - -def get_cache(obj: object, key: str, default: object = None): - return getattr(obj, f"__{key}", default) From f4cd389a6affcb2b7c1d7726f2fe33e2599407ea Mon Sep 17 00:00:00 2001 From: Soju06 Date: Tue, 10 Dec 2024 14:47:17 +0900 Subject: [PATCH 2/4] fix: missing type hints for KisChartBar --- pykis/api/stock/chart.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pykis/api/stock/chart.py b/pykis/api/stock/chart.py index fe72b3a9..3a13fb4f 100644 --- a/pykis/api/stock/chart.py +++ b/pykis/api/stock/chart.py @@ -1,7 +1,16 @@ import bisect from datetime import date, datetime, time, tzinfo from decimal import Decimal -from typing import Iterable, Literal, Protocol, TypeVar, overload, runtime_checkable +from typing import ( + TYPE_CHECKING, + Iterable, + Iterator, + Literal, + Protocol, + TypeVar, + overload, + runtime_checkable, +) from pykis.api.base.product import KisProductBase, KisProductProtocol from pykis.api.stock.market import MARKET_TYPE @@ -15,6 +24,9 @@ "TChart", ] +if TYPE_CHECKING: + from pandas import DataFrame + @runtime_checkable class KisChartBar(Protocol): @@ -142,9 +154,9 @@ def __iter__(self) -> Iterable[KisChartBar]: ... def __len__(self) -> int: ... - def __reversed__(self): ... + def __reversed__(self) -> Iterator[KisChartBar]: ... - def df(self): + def df(self) -> "DataFrame": """ 차트를 Pandas DataFrame으로 변환합니다. @@ -274,10 +286,10 @@ def __iter__(self) -> Iterable[KisChartBar]: def __len__(self) -> int: return len(self.bars) - def __reversed__(self): + def __reversed__(self) -> Iterator[KisChartBar]: return reversed(self.bars) - def df(self): + def df(self) -> "DataFrame": """ 차트를 Pandas DataFrame으로 변환합니다. From 950fa3662c67fd82de48969533d1f48f02cbeadc Mon Sep 17 00:00:00 2001 From: Soju06 Date: Tue, 10 Dec 2024 14:48:26 +0900 Subject: [PATCH 3/4] chore: absolute path based test code --- pykis/__env__.py | 2 +- tests/env.py | 9 ++++++--- tests/main.py | 9 ++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pykis/__env__.py b/pykis/__env__.py index f719495e..76cd2940 100644 --- a/pykis/__env__.py +++ b/pykis/__env__.py @@ -23,7 +23,7 @@ VERSION = "{{VERSION_PLACEHOLDER}}" # This is automatically set via a tag in GitHub Workflow. -VERSION = "dev" if "VERSION_PLACEHOLDER" in VERSION else VERSION +VERSION = "24+dev" if "VERSION_PLACEHOLDER" in VERSION else VERSION USER_AGENT = f"PyKis/{VERSION}" diff --git a/tests/env.py b/tests/env.py index 80d41515..fd1c1322 100644 --- a/tests/env.py +++ b/tests/env.py @@ -1,12 +1,15 @@ import os from typing import Literal -import dotenv - import pykis.logging from pykis import PyKis -dotenv.load_dotenv() +try: + import dotenv + + dotenv.load_dotenv() +except ImportError: + pass def load_pykis( diff --git a/tests/main.py b/tests/main.py index 868ab81f..19db317f 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,7 +1,11 @@ +import sys import unittest +from pathlib import Path -def test_main(): +def test_main() -> None: + sys.path.append(str(Path(__file__).parent.parent)) + loader = unittest.TestLoader() suite = loader.discover("tests/unit") @@ -10,4 +14,7 @@ def test_main(): if __name__ == "__main__": + if sys.version_info < (3, 10): + raise RuntimeError("Python 3.10 이상이 필요합니다.") + test_main() From 9bd0f9a3a6dc0a18728f4efbd7ab27462902b307 Mon Sep 17 00:00:00 2001 From: Soju06 Date: Tue, 10 Dec 2024 14:49:31 +0900 Subject: [PATCH 4/4] add: decorator keeping function information --- pykis/utils/repr.py | 4 +++- pykis/utils/thread_safe.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pykis/utils/repr.py b/pykis/utils/repr.py index 8dd3af75..e8e55a62 100644 --- a/pykis/utils/repr.py +++ b/pykis/utils/repr.py @@ -1,3 +1,4 @@ +from functools import wraps from io import StringIO from typing import Any, Iterable, Literal, Protocol, TypeVar @@ -41,6 +42,7 @@ def kis_repr( max_depth: int = 7, ): def decorator(cls: type[TObject]) -> type[TObject]: + @wraps(cls.__repr__) def __repr__(self, _depth: int = 0) -> str: return object_repr( self, @@ -57,7 +59,7 @@ def __repr__(self, _depth: int = 0) -> str: __repr__.__module__ = cls.__module__ __repr__.__qualname__ = f"{cls.__qualname__}.__repr__" __repr__.__name__ = "__repr__" - __repr__.__is_kis_repr__ = True + __repr__.__is_kis_repr__ = True # type: ignore cls.__repr__ = __repr__ return cls diff --git a/pykis/utils/thread_safe.py b/pykis/utils/thread_safe.py index 6745d628..31cae4de 100644 --- a/pykis/utils/thread_safe.py +++ b/pykis/utils/thread_safe.py @@ -1,3 +1,4 @@ +from functools import wraps from multiprocessing import Lock from typing import Any, Callable @@ -9,7 +10,8 @@ def thread_safe(name: str | None = None): - def decorator(fn: Callable[..., Any]): + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: + @wraps(fn) def wrapper(self, *args, **kwargs): with global_lock: key = f"__thread_safe_{name or fn.__name__}_lock"