From 8bb642f489bb7b5ceb14e5fc53505e28aa63d926 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:30:28 +0900 Subject: [PATCH 01/27] =?UTF-8?q?refactor:=20cache=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20convert=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/caches/cache.pkl | Bin 616 -> 1109 bytes tagoapi/caches/station.pkl | Bin 19197785 -> 19197785 bytes tagoapi/client.py | 50 +++++++++++++---------------------- tagoapi/models/BaseModel.py | 6 ++--- tagoapi/models/Station.py | 14 ++++++---- tagoapi/utils/convertor.py | 2 +- tagoapi/utils/get_station.py | 3 ++- tagoapi/utils/http.py | 5 ++-- test_station.py | 13 +++++++++ tests/test_station.py | 4 --- 10 files changed, 48 insertions(+), 49 deletions(-) create mode 100644 test_station.py delete mode 100644 tests/test_station.py diff --git a/tagoapi/caches/cache.pkl b/tagoapi/caches/cache.pkl index 48379cc76c7a9fa34994b8d07319d97bf9f501e4..63f77ed6036d97100c2ff1549bc2cf6791c0d280 100644 GIT binary patch literal 1109 zcmZ{jPfQa*6vk`#(^5dA1~_1hF-kN|VB69{Nk}YEDhNx;A5jugw%xUFvAf%LciTc@ zg2Z@nj2efC9=Mu#^FWNz6BonLg9lCYz)gyJHSyxi&K4}iJ-vPJef#?|`^`%2+GwrA zdhXy$sAb9!3|UkAhN1&K9Ovb@P&db6I;zVHX;ZRFXDXwo$sUeg3IF`&`1qy`SuIUX zk=DX#|NMoG2&z{#QIbWBnkzrX2C9Nl)86X)(!;gg&Bs4pAZ+(FJbs%uQH^1w@#sol zg9EvBAuow>Lsldl4SxxAte=ep%%i*pm{+JFhM|tQ`{jNzwe$z-sVi^C_u?mT{%eQs zLjyX&eC*wOU3&0#Z*2oZKw^hws_qlPt6vLP`@da;HM(X}!}<4Mxs#^%P&e%d!$rre#SrFgjiq zl^gEqaCBmjP3?VDE0_k5WR!Q=tBY=i>7=~Z7}up-UQSBaq$P}Q6xHs@>m#5Vfmoo+ zlkAxbMrMX1<9WGQ=vtiYy)`XNggm*h=Vo?HC=_OcQ~8kO_a{bScqlfTUF1Dk%`;n+ zgI9wwWm)eX3D1eSfkJ;j))RVy1#?8i;9@1OU~t=|6;~_%W7Rietnx;b$>6XhAA+_sf0UY3%@>p&^dI}$tQ*uNv0Wz+2Z3;x-fS^xk5 literal 616 zcmZo*nHtN)00y;FGtQY_$(iES5Xi{T!&RJEmYNb@l9`)2#oh6iYH5|@1E10!;UJK4Rxa*N z#s&rkW~Lw#*^C~MlEn1<#DYw{-29Z(oMJt&`YAoE5PI?y%|@_tcCfQNQ>OIrqMFqM zk@3r);@isv<%ASW>EU^`q2*1>g4df`-gGUQ(!-gWm*NMs!7q18kIYGY&``|1yNJ`YJ=TNA7ih%j>2UJ|0G|yH|kpOl^7F)AP1#2h`&k;(i$t zex*qnl3oo!7iUO;Bd_hPR%dCCD)#hXqy~%va|0to15*QFvH-~HVAM?+{)$_OP5l@w7lG*Fr-6j7H{Pl~4Y(pH*g!%B;+@+mEm zmQ_-r+;eW>hN%UG`;QfhD5{v^N+_w60;QEvRypNWP*Ek7RZ&$n)zwf_Ew$BAS3UJL z&`=|dHPKWv2RP6{4%XZuT4<@2LmlRDM>tYzZ5-ujZ5`uS?X=fHN5|=;vo4Nzg04=~ z%}Gvnic@uWn$w-3hcoqbmb3NJ+d2B^>s;sQr@!-E;6fJ};9{4!)MW-5WU$L!VTdbT z^a+2Uuv_|ZBg;gVjVu>gKC(h&#mGvLl_RS}R*kF{Sv|5wWX;H0k+mc1MAnV07g;~DL1e?o bMv;vpo5Z}P(PL{tn`TRwcbYPMXsiDL#}OoY delta 641 zcmWN=2bc&17y!_I`|QoxoHQw^sDz7sB=4GTw$|%|!4Vp;HxQbHR zAtO!2`@UDQuDGCN_c1~#rIk@uIptMQprT4DtD>rEs;i-p1=6(-&&_f>fi0Ni{)J%_g+$^)rG1n8GG|zlbdD=6c^_=Iu;6*Qa*(+Z4ngteG z;%)DE*L&Xgfu)vN?n5hlw*R zL>5F=jI0z{IkHM*)yQg*)gx;}){LwbSv#^$WZlSmk@X`BBO63EjBFIyII>A()5vC# a%_Cbx7R7>=(S374tEqbw4wyG=Xq#QZpChCI diff --git a/tagoapi/client.py b/tagoapi/client.py index f8e5ff2..6a8e2e0 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -155,44 +155,32 @@ def _fetch_and_convert( cache_key = KeyExtract(model) response = parse_metadata(self._get(endpoint, params)) if not response: - return None + return None ## Convert to List if isinstance(response, list): + for res in response: + key = cache_key.generate_key(res) + cache.save(key, res, self.CACHE_TTL) + + return convert(response, model.from_list, self) - if not is_cache: - return convert(response, model.from_list, self) - - result = [] - for v in response: - key = cache_key.generate_key(v) - cached = cache.get(key) - if cached: - result.append(cached) - else: - parsed_obj = convert(v, model.from_dict) - result.append(parsed_obj) - cache.save(key, parsed_obj, self.CACHE_TTL) - - return result - - ## Covert to Dict - else: - - if is_cache: - key = cache_key.generate_key(response) - cached = cache.get(key) - if cached: - return [cached] if is_list else cached - result = convert(response, model.from_dict) - cache.save(key, result, self.CACHE_TTL) - return [result] if is_list else result + ## Convert to Dict + key = cache_key.generate_key(response) + cache.save(key, response, self.CACHE_TTL) + + result = convert(response, model.from_dict, self) + return [result] if is_list else result + def _get(self, endpoint: str, params: dict) -> any: response = http_get(f"{self.BASE_URL}/{endpoint}", params=params) error_code = response.get("returnReasonCode") - + + if not error_code: + return response + if error_code == '20': raise ServiceAccessDeniedError("서비스에 접근이 거부되었습니다.") elif error_code == '22': @@ -204,6 +192,4 @@ def _get(self, endpoint: str, params: dict) -> any: elif error_code == '32': raise UnRegisteredIpError("등록되지 않은 IP입니다.") elif error_code: - raise RuntimeError(f"실행중 오류가 발생했습니다. 에러코드: {error_code}") - - return response \ No newline at end of file + raise RuntimeError(f"실행중 오류가 발생했습니다. 에러코드: {error_code}") \ No newline at end of file diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index 92a3c47..b0a18dc 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -4,7 +4,7 @@ class BaseModel: cache_key = "BaseModel:" - def __init__(self, client: TAGOClient): + def __init__(self, client: "TAGOClient"): self._client = client def to_dict(self) -> dict: ... @@ -13,10 +13,10 @@ def to_dict(self): return vars(self) @classmethod - def from_dict(cls, data: dict, client: TAGOClient) -> "BaseModel": ... + def from_dict(cls, data: dict, client: "TAGOClient") -> "BaseModel": ... @classmethod - def from_list(cls, data: list, client: TAGOClient) -> list["BaseModel"]: + def from_list(cls, data: list, client: "TAGOClient") -> list["BaseModel"]: return [cls.from_dict(v, client) for v in data] diff --git a/tagoapi/models/Station.py b/tagoapi/models/Station.py index 1e5c335..d7f5897 100644 --- a/tagoapi/models/Station.py +++ b/tagoapi/models/Station.py @@ -2,6 +2,7 @@ from .BaseModel import BaseModel if TYPE_CHECKING: from .Route import Route + from tagoapi import TAGOClient class Station(BaseModel): # cache_key = "Station:" @@ -16,9 +17,11 @@ def __init__( gpsLong: float = None, cityCode: int = None, updowncd: int = None, - nodeord: int = None + nodeord: int = None, + client: "TAGOClient" = None # *routeList: list['Route'] ): + super().__init__(client) self.nodeId = nodeId self.nodeNm = nodeNm self.nodeNo = nodeNo @@ -36,7 +39,7 @@ def to_dict(self): return vars(self) @classmethod - def from_dict(cls, data: dict) -> "Station": + def from_dict(cls, data: dict, client: "TAGOClient") -> "Station": return cls( nodeId = data.get("nodeid"), nodeNm = data.get("nodenm"), @@ -45,10 +48,11 @@ def from_dict(cls, data: dict) -> "Station": gpsLong = float(data.get("gpslong")), cityCode = data.get("citycode"), updowncd = data.get("updowncd"), - nodeord = data.get("nodeord") + nodeord = data.get("nodeord"), + client = client ) @classmethod - def from_list(cls, data: list[dict]) -> list["Station"]: - return [cls.from_dict(station) for station in data] + def from_list(cls, data: list[dict], client: "TAGOClient") -> list["Station"]: + return [cls.from_dict(station, client) for station in data] \ No newline at end of file diff --git a/tagoapi/utils/convertor.py b/tagoapi/utils/convertor.py index 8ca7411..b59a336 100644 --- a/tagoapi/utils/convertor.py +++ b/tagoapi/utils/convertor.py @@ -4,7 +4,7 @@ T = TypeVar('T', dict, list) # list, dict으로 매개변수 받을 때 U = TypeVar('U', dict[str], list) # list, dict으로 반환할 때 -def convert(res: T, converter: Callable[[T, TAGOClient], U], _client: TAGOClient) -> U: +def convert(res: T, converter: Callable[[T, "TAGOClient"], U], _client: "TAGOClient") -> U: if not res: return None return converter(res, _client) diff --git a/tagoapi/utils/get_station.py b/tagoapi/utils/get_station.py index 1414a73..6abd36e 100644 --- a/tagoapi/utils/get_station.py +++ b/tagoapi/utils/get_station.py @@ -11,6 +11,7 @@ def csv_to_dict(csvfile, encoding): PROJECT_ROOT = os.path.abspath(os.path.join(MODULE_DIR, "..")) CACHE_DIR = os.path.join(PROJECT_ROOT, "caches") STATION_CACHE_PATH = os.path.join(CACHE_DIR, "station.pkl") + cache = Cache(STATION_CACHE_PATH) station_list = cache.get("stations_2025_06_15.csv") if not station_list: @@ -18,7 +19,7 @@ def csv_to_dict(csvfile, encoding): cache.save("stations_2025_06_15.csv", station_list) def get_station(keyword) -> list[Station]: - print(cache.current_cache) + # print(cache.current_cache) result = [] try: for station in station_list: diff --git a/tagoapi/utils/http.py b/tagoapi/utils/http.py index 0b636b2..1e3cefd 100644 --- a/tagoapi/utils/http.py +++ b/tagoapi/utils/http.py @@ -13,7 +13,8 @@ def http_get(endpoint: str, params: dict) -> dict: except ConnectionError: raise RuntimeError("서버에 연결할 수 없습니다. 인터넷 연결이나 도메인을 확인해주세요.") except Timeout: - raise RuntimeError("요청 시간이 초가되었습니다.") + raise RuntimeError("요청 시간이 초과되었습니다.") + except HTTPError as e: raise RuntimeError(f"HTTP 오류 발생: {e.response.status_code}") except JSONDecodeError as e: @@ -21,7 +22,5 @@ def http_get(endpoint: str, params: dict) -> dict: return xmltodict.parse(response.text).get("OpenAPI_ServiceResponse", {}).get("cmmMsgHeader", {}) except Exception as e: raise ValueError("응답을 JSON으로 디코딩 할 수 없습니다.") - - except RequestException as e: raise RuntimeError(f"요청 중 알 수 없는 오류 발생: {e}") \ No newline at end of file diff --git a/test_station.py b/test_station.py new file mode 100644 index 0000000..8f09a22 --- /dev/null +++ b/test_station.py @@ -0,0 +1,13 @@ +from tagoapi import * +import os +from dotenv import load_dotenv + +load_dotenv() +key = os.environ.get("TAGO_API_KEY") +client = TAGOClient(TAGOAuth(key)) + +print(cache.current_cache) + +# print(client.get_station(22, nodeNm="삼덕")) + +# print(get_station("대구소프트웨어마이스터고등학교")) diff --git a/tests/test_station.py b/tests/test_station.py deleted file mode 100644 index 5592564..0000000 --- a/tests/test_station.py +++ /dev/null @@ -1,4 +0,0 @@ -from tagoapi import * - - -get_station("adf") \ No newline at end of file From 4ab47298f80bdddb0ffc22767c6337d0000b54c3 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:57:08 +0900 Subject: [PATCH 02/27] =?UTF-8?q?test:=20cache=20=ED=83=9C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/caches/cache.pkl | Bin 1109 -> 529 bytes tagoapi/client.py | 2 +- tagoapi/utils/cache.py | 4 +++- test_station.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tagoapi/caches/cache.pkl b/tagoapi/caches/cache.pkl index 63f77ed6036d97100c2ff1549bc2cf6791c0d280..68421c3325832e2d7a562e5726c7db68dff36a70 100644 GIT binary patch delta 51 zcmcc0F_DF}fn_Qi(?r(wtm=LI4;UxzEC4ayOq;C7mXh0k8X3&7Q#5+SN)pra6ALo+a)I(W#d^UI z4O4p9VXVnhG#f$cd)VWXb23x&N~ZMi!8E}HxI!G={hcAQ^-v8xEQzHh8B=a>&y2hh(?C;w?PT*PGuKczSKqSCigNv| zAd9q+M1M=|B1i4WLZ8I)@^G`@GRstB<0P*PH}Nj+?!fbdM2lEDFC`sGf^;DR#qr}$?3^{}TG6#D>!ZHl{t)|LZNXakHOHKi@+3P_Tpg^=J$U& z6bI@&1kw3w)uZW_xnP~5TNT*vx*+KcK(kaDs8t52^(0j5?it4)H9@p4FFaZB14(OY Z29H-mAR|Ktue;+dQ(!tOs46Yh0{~%T?QH-6 diff --git a/tagoapi/client.py b/tagoapi/client.py index 6a8e2e0..693c9cf 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -72,7 +72,7 @@ def get_station_by_route( params= build_params(self.auth, cityCode=cityCode, routeId=routeId) return self._fetch_and_convert(endpoint, params, Station) - @from_cache_or_fetch(86400) + # @from_cache_or_fetch(86400) def get_station( self, cityCode: int, diff --git a/tagoapi/utils/cache.py b/tagoapi/utils/cache.py index e6fdb0d..25dca04 100644 --- a/tagoapi/utils/cache.py +++ b/tagoapi/utils/cache.py @@ -6,6 +6,7 @@ CACHE_DIR = os.path.join(PROJECT_ROOT, "caches") DEFAULT_CACHE_PATH = os.path.join(CACHE_DIR, "cache.pkl") + class Cache: def __init__(self, path: str = DEFAULT_CACHE_PATH): self.path = os.path.abspath(path) @@ -19,7 +20,8 @@ def _load(self) -> dict: with open(self.path, 'rb') as f: return pickle.load(f) - except Exception: + except Exception as e: + # print(e) return {} return {} diff --git a/test_station.py b/test_station.py index 8f09a22..0430f7b 100644 --- a/test_station.py +++ b/test_station.py @@ -8,6 +8,6 @@ print(cache.current_cache) -# print(client.get_station(22, nodeNm="삼덕")) +print(client.get_station(22, nodeNm="삼덕")) # print(get_station("대구소프트웨어마이스터고등학교")) From ce66d92c12ab0c3d8e50608842399a6898561a20 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:57:56 +0900 Subject: [PATCH 03/27] docs: Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d56181..c223c9f 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,8 @@ print(stations) #### 공통 메서드 ~~~python obj.to_dict() # 객체 → dict 변환 -ClassName.from_dict(dict) # dict → 객체 변환 -ClassName.from_list(list) # dict 리스트 → 객체 리스트 변환 +BaseModel.from_dict(dict) # dict → 객체 변환 +BaseModel.from_list(list) # dict 리스트 → 객체 리스트 변환 ~~~ --- @@ -153,4 +153,4 @@ ClassName.from_list(list) # dict 리스트 → 객체 리스트 변환 --- ### 5. 오류 및 이슈 -버그 제보 또는 기능 요청은 [GitHub 이슈](https://github.com/hyuntroll/TAGOBus-API/issues)에 등록해주세요. \ No newline at end of file +버그 제보 또는 기능 요청은 [GitHub 이슈](https://github.com/hyuntroll/TAGOBus-API/issues)에 등록해주세요. From 67c48d3237bae02fc798c1ab9a90d1634a31839b Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:09:23 +0900 Subject: [PATCH 04/27] feat: create BaseList extends list --- .idea/.gitignore | 5 +++++ tagoapi/models/BaseList.py | 13 +++++++++++++ tagoapi/models/BaseModel.py | 19 ++++++++++++++----- 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 tagoapi/models/BaseList.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/tagoapi/models/BaseList.py b/tagoapi/models/BaseList.py new file mode 100644 index 0000000..8ede21c --- /dev/null +++ b/tagoapi/models/BaseList.py @@ -0,0 +1,13 @@ +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from tagoapi.models.BaseModel import BaseModel + from tagoapi import TAGOClient + +class BaseList(list): # list 상속 받아도 좋을듯 + + def as_list(self) -> list["BaseModel"]: + return list(self) + + def set_client(self, client: "TAGOClient"): + for element in self: + element.set_client(client) \ No newline at end of file diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index b0a18dc..07f3041 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -1,22 +1,31 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from tagoapi import TAGOClient + from tagoapi.models.BaseList import BaseList class BaseModel: cache_key = "BaseModel:" - def __init__(self, client: "TAGOClient"): - self._client = client + def __init__(self): + self._client = None def to_dict(self) -> dict: ... def to_dict(self): return vars(self) + def set_client(self, client: "TAGOClient"): + self._client = client + @classmethod - def from_dict(cls, data: dict, client: "TAGOClient") -> "BaseModel": ... + def from_dict(cls, data: dict) -> "BaseModel": ... @classmethod - def from_list(cls, data: list, client: "TAGOClient") -> list["BaseModel"]: - return [cls.from_dict(v, client) for v in data] + def from_list(cls, data: list) -> "BaseList": + model = BaseList() + for element in data: + model.append(cls.from_dict(element)) + + return model + # return [cls.from_dict(v) for v in data] From 637f8b962a274efaaeba27385fb06ce1a8d7374d Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:11:08 +0900 Subject: [PATCH 05/27] refactor: renamed from http.py --- tagoapi/utils/{http.py => get.py} | 1 + 1 file changed, 1 insertion(+) rename tagoapi/utils/{http.py => get.py} (98%) diff --git a/tagoapi/utils/http.py b/tagoapi/utils/get.py similarity index 98% rename from tagoapi/utils/http.py rename to tagoapi/utils/get.py index 1e3cefd..98509bf 100644 --- a/tagoapi/utils/http.py +++ b/tagoapi/utils/get.py @@ -6,6 +6,7 @@ def http_get(endpoint: str, params: dict) -> dict: + response = None try: response = requests.get(endpoint, params=params, timeout=(3, 10)) response.raise_for_status() From 6a04dbed9148bbccf315cac6b0d909b25966a22f Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:11:47 +0900 Subject: [PATCH 06/27] =?UTF-8?q?refactor:=20client=20=EB=B0=8F=20cache=5F?= =?UTF-8?q?util=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + tagoapi/client.py | 42 ++++-------- tagoapi/models/Route.py | 3 +- tagoapi/models/Station.py | 4 +- tagoapi/utils/__init__.py | 2 +- tagoapi/utils/cache.py | 22 ++++++- tagoapi/utils/cache_util.py | 35 +++++++--- tagoapi/utils/convertor.py | 6 +- tagoapi/utils/get_station.py | 82 ++++++++++++------------ test_station.py => tests/test_station.py | 2 +- 10 files changed, 108 insertions(+), 91 deletions(-) rename test_station.py => tests/test_station.py (85%) diff --git a/.gitignore b/.gitignore index 7ccf1b3..33e04cb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ tests/__pycache__ .env caches/cache.pkl .vscodeoutput.json +.tagoapi tagoapi/caches/station.pkl \ No newline at end of file diff --git a/tagoapi/client.py b/tagoapi/client.py index 693c9cf..9b6c3ef 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -72,12 +72,15 @@ def get_station_by_route( params= build_params(self.auth, cityCode=cityCode, routeId=routeId) return self._fetch_and_convert(endpoint, params, Station) - # @from_cache_or_fetch(86400) + @from_cache_or_fetch(86400) def get_station( self, cityCode: int, nodeNo: int = None, nodeNm: str = None, + _is_cache: bool = True, + _model: BaseModel = Station, + _is_list: bool = True ) -> list[Station]: """정류소명 또는 번호로 정류소를 조회합니다""" if not (nodeNo or nodeNm): @@ -85,7 +88,7 @@ def get_station( endpoint = f'{self.BUSTATION}/getSttnNoList' params= build_params(self.auth, cityCode=cityCode, nodeNm=nodeNm,nodeNo=nodeNo) - return self._fetch_and_convert(endpoint, params, Station) + return self._fetch_and_convert(endpoint, params) @from_cache_or_fetch(86400) def get_station_by_gps( @@ -142,36 +145,17 @@ def get_route_pos_near_station( params = build_params(self.auth, cityCode=cityCode, routeId=routeId, nodeId=nodeId) return self._fetch_and_convert(endpoint, params, Vehicle) - - def _fetch_and_convert( - self, - endpoint: str, - params: dict, - model: BaseModel, - is_list: bool = True, - is_cache: bool = True - ) -> BaseModel: - cache_key = KeyExtract(model) + self, + endpoint: str, + params: dict, + ) -> list | dict: response = parse_metadata(self._get(endpoint, params)) - if not response: - return None - - ## Convert to List - if isinstance(response, list): - for res in response: - key = cache_key.generate_key(res) - cache.save(key, res, self.CACHE_TTL) - - return convert(response, model.from_list, self) + return response - ## Convert to Dict - key = cache_key.generate_key(response) - cache.save(key, response, self.CACHE_TTL) - result = convert(response, model.from_dict, self) - return [result] if is_list else result + ############################# def _get(self, endpoint: str, params: dict) -> any: @@ -185,11 +169,11 @@ def _get(self, endpoint: str, params: dict) -> any: raise ServiceAccessDeniedError("서비스에 접근이 거부되었습니다.") elif error_code == '22': raise RequestExcessdsError("서비스 요청제한횟수를 초과했습니다.") - if error_code == '30': + elif error_code == '30': raise ServiceKeyNotRegisteredError("유효하지 않는 서비스키 입니다.") elif error_code == '31': raise DeadLineHasExpired("API활용기간이 만료되었습니다.") elif error_code == '32': raise UnRegisteredIpError("등록되지 않은 IP입니다.") - elif error_code: + else: raise RuntimeError(f"실행중 오류가 발생했습니다. 에러코드: {error_code}") \ No newline at end of file diff --git a/tagoapi/models/Route.py b/tagoapi/models/Route.py index c25b5d2..688c85f 100644 --- a/tagoapi/models/Route.py +++ b/tagoapi/models/Route.py @@ -18,10 +18,9 @@ def __init__( startNodeNm: str = None, endvehicletime: int = None, startvehicletime: int = None, - client = None #TODO: 정류장 리스트도 넣으면 좋을 듯 합니당 ): - super().__init__(client) + super().__init__() self.routeId = routeId self.routeNo = routeNo diff --git a/tagoapi/models/Station.py b/tagoapi/models/Station.py index d7f5897..c41fc64 100644 --- a/tagoapi/models/Station.py +++ b/tagoapi/models/Station.py @@ -18,10 +18,9 @@ def __init__( cityCode: int = None, updowncd: int = None, nodeord: int = None, - client: "TAGOClient" = None # *routeList: list['Route'] ): - super().__init__(client) + super().__init__() self.nodeId = nodeId self.nodeNm = nodeNm self.nodeNo = nodeNo @@ -49,7 +48,6 @@ def from_dict(cls, data: dict, client: "TAGOClient") -> "Station": cityCode = data.get("citycode"), updowncd = data.get("updowncd"), nodeord = data.get("nodeord"), - client = client ) @classmethod diff --git a/tagoapi/utils/__init__.py b/tagoapi/utils/__init__.py index ceaca27..2addcfe 100644 --- a/tagoapi/utils/__init__.py +++ b/tagoapi/utils/__init__.py @@ -5,7 +5,7 @@ from .convertor import convert from .parser import KeyExtract from .params import build_params -from .http import http_get +from .get import http_get from .get_station import get_station diff --git a/tagoapi/utils/cache.py b/tagoapi/utils/cache.py index 25dca04..427c187 100644 --- a/tagoapi/utils/cache.py +++ b/tagoapi/utils/cache.py @@ -8,6 +8,11 @@ class Cache: + """ + TTL 기반 캐시. 저장 값은 항상 dict/list[dict]. + 도메인 객체는 캐시에서 꺼낼 때 변환 후 client 주입. + """ + def __init__(self, path: str = DEFAULT_CACHE_PATH): self.path = os.path.abspath(path) os.makedirs(os.path.dirname(self.path), exist_ok=True) @@ -26,16 +31,20 @@ def _load(self) -> dict: return {} def save(self, key: str, value: dict, ttl: int = 86400) -> bool: + """ + value: dict 또는 list[dict]만 허용 + """ + if not isinstance(value, (dict, list)): + raise TypeError("value must be a dict or list") self._cache[key] = { "value": value, "ttl": ttl, "saved_time": time() } - with open(self.path, 'wb') as f: - pickle.dump(self._cache, f) + self._dump() return True - def get(self, key: str) -> dict | None: + def get(self, key: str): entry = self._cache.get(key) if not entry: return None @@ -46,6 +55,13 @@ def get(self, key: str) -> dict | None: return None return value + def _dump(self): + try: + with open(self.path, "wb") as f: + pickle.dump(self._cache, f) + except Exception as e: + print(f"[Cache] dump error: {e}") + @property def current_cache(self): return self._cache diff --git a/tagoapi/utils/cache_util.py b/tagoapi/utils/cache_util.py index 507272e..00e0252 100644 --- a/tagoapi/utils/cache_util.py +++ b/tagoapi/utils/cache_util.py @@ -1,3 +1,5 @@ +from unittest import result + from .cache import Cache @@ -7,14 +9,31 @@ def from_cache_or_fetch(ttl: int = 86400): # 데코레이터가 사용할 매개변수 def real_deco(fn): # 호출할 함수를 매개변수로 받음 def wrapper(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아서 이를 실행 - key = generate_cache_key(*args, _fname=fn.__name__, **kwargs) - cached = cache.get(key) - if cached: - return cached - - result = fn(self, *args, **kwargs) - cache.save(key, result, ttl) - return result + + model = kwargs.get('_model') or getattr(fn, '_model', None) + is_list = kwargs.get('_is_list', True) + + if kwargs.get("_is_cache", True): + key = generate_cache_key(*args, _fname=fn.__name__, **kwargs) + cached = cache.get(key) + + # 만약 캐시가 없다면 + if not cached: + cached = fn(self, *args, **kwargs) + cache.save(key, cached, ttl) + else: + cached = fn(self, *args, **kwargs) + + ## convert list + if isinstance(cached, list): + res = model.from_list(cached) + res.set_client(self) + return res + else: + res = model.from_dict(cached) + res.set_client(self) + return res if not is_list else [res] + return wrapper return real_deco diff --git a/tagoapi/utils/convertor.py b/tagoapi/utils/convertor.py index b59a336..84fbcaa 100644 --- a/tagoapi/utils/convertor.py +++ b/tagoapi/utils/convertor.py @@ -1,10 +1,10 @@ from typing import TYPE_CHECKING, Union, Callable, TypeVar, Any if TYPE_CHECKING: - from tagoapi import TAGOClient + from tagoapi.models import BaseModel, BaseList T = TypeVar('T', dict, list) # list, dict으로 매개변수 받을 때 U = TypeVar('U', dict[str], list) # list, dict으로 반환할 때 -def convert(res: T, converter: Callable[[T, "TAGOClient"], U], _client: "TAGOClient") -> U: +def convert(res: T, converter: Callable[[T], U]) -> Union["BaseModel", "BaseList"]: if not res: return None - return converter(res, _client) + return converter(res) diff --git a/tagoapi/utils/get_station.py b/tagoapi/utils/get_station.py index 6abd36e..17863f4 100644 --- a/tagoapi/utils/get_station.py +++ b/tagoapi/utils/get_station.py @@ -2,44 +2,44 @@ from tagoapi.models import Station from tagoapi.exceptions import * from .cache import Cache - -def csv_to_dict(csvfile, encoding): - loaded_csv = pd.read_csv(csvfile, encoding=encoding) - data = loaded_csv.to_dict(orient='records') - return data -MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) -PROJECT_ROOT = os.path.abspath(os.path.join(MODULE_DIR, "..")) -CACHE_DIR = os.path.join(PROJECT_ROOT, "caches") -STATION_CACHE_PATH = os.path.join(CACHE_DIR, "station.pkl") - -cache = Cache(STATION_CACHE_PATH) -station_list = cache.get("stations_2025_06_15.csv") -if not station_list: - station_list = csv_to_dict("tagoapi/csv/stations_2025_06_15.csv", 'cp949') - cache.save("stations_2025_06_15.csv", station_list) - -def get_station(keyword) -> list[Station]: - # print(cache.current_cache) - result = [] - try: - for station in station_list: - if keyword in station["정류장명"]: - result.append( - Station.from_dict({ - "nodeid": station["정류장번호"], - "nodenm": station["정류장명"], - "nodeno": station["모바일단축번호"], - "gpslati": station["위도"], - "gpslong": station["경도"], - "citycode": station["도시코드"] - }) - ) - - - return result - except TypeError as e: - raise CacheNotFoundError("정류소 캐시파일을 불러오지 못했습니다.") - - - - +# +# def csv_to_dict(csvfile, encoding): +# loaded_csv = pd.read_csv(csvfile, encoding=encoding) +# data = loaded_csv.to_dict(orient='records') +# return data +# MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) +# PROJECT_ROOT = os.path.abspath(os.path.join(MODULE_DIR, "..")) +# CACHE_DIR = os.path.join(PROJECT_ROOT, "caches") +# STATION_CACHE_PATH = os.path.join(CACHE_DIR, "station.pkl") +# +# cache = Cache(STATION_CACHE_PATH) +# station_list = cache.get("stations_2025_06_15.csv") +# if not station_list: +# station_list = csv_to_dict("tagoapi/csv/stations_2025_06_15.csv", 'cp949') +# cache.save("stations_2025_06_15.csv", station_list) +# +def get_station(keyword) -> list[Station]: pass +# # print(cache.current_cache) +# result = [] +# try: +# for station in station_list: +# if keyword in station["정류장명"]: +# result.append( +# Station.from_dict({ +# "nodeid": station["정류장번호"], +# "nodenm": station["정류장명"], +# "nodeno": station["모바일단축번호"], +# "gpslati": station["위도"], +# "gpslong": station["경도"], +# "citycode": station["도시코드"] +# }) +# ) +# +# +# return result +# except TypeError as e: +# raise CacheNotFoundError("정류소 캐시파일을 불러오지 못했습니다.") +# +# +# +# diff --git a/test_station.py b/tests/test_station.py similarity index 85% rename from test_station.py rename to tests/test_station.py index 0430f7b..75d1aef 100644 --- a/test_station.py +++ b/tests/test_station.py @@ -1,4 +1,4 @@ -from tagoapi import * +from tagoapi import TAGOClient, TAGOAuth, cache import os from dotenv import load_dotenv From 61aacb12a2c676aa878a825ce9a6cac58b298afd Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:49:37 +0900 Subject: [PATCH 07/27] =?UTF-8?q?refactor:=20convert=5Fmodel=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/__init__.py | 4 +-- tagoapi/utils/__init__.py | 4 +-- tagoapi/utils/cache_util.py | 56 +++++++++++++++++++------------------ 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/tagoapi/__init__.py b/tagoapi/__init__.py index de46ab4..46ccbf3 100644 --- a/tagoapi/__init__.py +++ b/tagoapi/__init__.py @@ -8,11 +8,11 @@ from .models import Station # from .utils import get_city_code -from .utils.cache_util import from_cache_or_fetch +from .utils.cache_util import covert_model from .utils.parser import KeyExtract from .utils.cache_util import cache from .utils.get_station import get_station -__all__ = [ 'TAGOClient', 'TAGOAuth', 'from_cache_or_fetch', 'Route', 'Vehicle', 'Station', 'KeyExtract', 'cache', 'get_station' ] \ No newline at end of file +__all__ = [ 'TAGOClient', 'TAGOAuth', 'covert_model', 'Route', 'Vehicle', 'Station', 'KeyExtract', 'cache', 'get_station' ] \ No newline at end of file diff --git a/tagoapi/utils/__init__.py b/tagoapi/utils/__init__.py index 2addcfe..65ab6a6 100644 --- a/tagoapi/utils/__init__.py +++ b/tagoapi/utils/__init__.py @@ -1,5 +1,5 @@ from .cache import Cache -from .cache_util import from_cache_or_fetch +from .cache_util import covert_model from .cache_util import cache from .parser import parse_metadata from .convertor import convert @@ -13,7 +13,7 @@ __all__ = [ "Cache", "KeyExtract", - "from_cache_or_fetch", + "covert_model", "parse_metadata", "convert", "build_params", diff --git a/tagoapi/utils/cache_util.py b/tagoapi/utils/cache_util.py index 00e0252..3876244 100644 --- a/tagoapi/utils/cache_util.py +++ b/tagoapi/utils/cache_util.py @@ -1,44 +1,46 @@ -from unittest import result - +from typing import TYPE_CHECKING from .cache import Cache +from ..models.BaseList import BaseList + +if TYPE_CHECKING: + from tagoapi.models import BaseModel cache = Cache() # method에서만 사용할 함수 -def from_cache_or_fetch(ttl: int = 86400): # 데코레이터가 사용할 매개변수 - def real_deco(fn): # 호출할 함수를 매개변수로 받음 - def wrapper(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아서 이를 실행 - - model = kwargs.get('_model') or getattr(fn, '_model', None) - is_list = kwargs.get('_is_list', True) +def covert_model(ttl: int = 86400, model: type["BaseModel"] = None, is_list: bool = True): # 데코레이터가 사용할 매개변수 + def decorator(fn): # 호출할 함수를 매개변수로 받음 + def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아서 이를 실행 + is_cached = kwargs.pop("_is_cached", True) - if kwargs.get("_is_cache", True): - key = generate_cache_key(*args, _fname=fn.__name__, **kwargs) - cached = cache.get(key) + key = _make_cache_key(*args, _fname=fn.__name__, **kwargs) if is_cached else None + cached = cache.get(key) if key else None - # 만약 캐시가 없다면 - if not cached: - cached = fn(self, *args, **kwargs) - cache.save(key, cached, ttl) - else: - cached = fn(self, *args, **kwargs) - ## convert list - if isinstance(cached, list): - res = model.from_list(cached) - res.set_client(self) - return res + if cached is None: + raw = fn(self, *args, **kwargs) + if key: + cache.save(key, raw, ttl) else: - res = model.from_dict(cached) + raw = cached + + if model: + ## convert list + if isinstance(raw, list): + res = model.from_list(cached) + else: + res = model.from_dict(cached) res.set_client(self) - return res if not is_list else [res] + + return res if is_list and not isinstance(res, BaseList) else BaseList(res) - return wrapper - return real_deco + return raw + return inner + return decorator -def generate_cache_key(*args, _fname: str, **kwargs) -> str: +def _make_cache_key(*args, _fname: str, **kwargs) -> str: return _fname + ":" + "&".join([str(a) for a in args]) + "&".join( f"{key}={value}" for key, value in kwargs.items() ) ## str로 나타낼 수 없으면 다르게 표시하도록 \ No newline at end of file From 6f0c2af878a10b092d3b9c593f13949b35882230 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:50:14 +0900 Subject: [PATCH 08/27] =?UTF-8?q?chore:=20BaseList=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=98=95=ED=83=9C=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EB=B0=8F=20client=20=EC=A3=BC=EC=9E=85=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/models/BaseList.py | 4 +++- tagoapi/models/BaseModel.py | 19 +++++++++---------- tagoapi/models/Station.py | 8 ++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tagoapi/models/BaseList.py b/tagoapi/models/BaseList.py index 8ede21c..47be0a6 100644 --- a/tagoapi/models/BaseList.py +++ b/tagoapi/models/BaseList.py @@ -10,4 +10,6 @@ def as_list(self) -> list["BaseModel"]: def set_client(self, client: "TAGOClient"): for element in self: - element.set_client(client) \ No newline at end of file + element.set_client(client) + + return self \ No newline at end of file diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index 07f3041..8a91b97 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -1,16 +1,14 @@ from typing import TYPE_CHECKING +from tagoapi.models.BaseList import BaseList if TYPE_CHECKING: from tagoapi import TAGOClient - from tagoapi.models.BaseList import BaseList class BaseModel: cache_key = "BaseModel:" def __init__(self): self._client = None - def to_dict(self) -> dict: ... - - def to_dict(self): + def to_dict(self) -> dict: return vars(self) def set_client(self, client: "TAGOClient"): @@ -20,12 +18,13 @@ def set_client(self, client: "TAGOClient"): def from_dict(cls, data: dict) -> "BaseModel": ... @classmethod - def from_list(cls, data: list) -> "BaseList": - model = BaseList() - for element in data: - model.append(cls.from_dict(element)) + def from_list(cls, data: list) -> BaseList: + return BaseList([cls.from_dict(d) for d in data]) + # model = BaseList() + # for element in data: + # model.append(cls.from_dict(element)) + # + # return model - return model - # return [cls.from_dict(v) for v in data] diff --git a/tagoapi/models/Station.py b/tagoapi/models/Station.py index c41fc64..670d952 100644 --- a/tagoapi/models/Station.py +++ b/tagoapi/models/Station.py @@ -38,7 +38,7 @@ def to_dict(self): return vars(self) @classmethod - def from_dict(cls, data: dict, client: "TAGOClient") -> "Station": + def from_dict(cls, data: dict) -> "Station": return cls( nodeId = data.get("nodeid"), nodeNm = data.get("nodenm"), @@ -50,7 +50,7 @@ def from_dict(cls, data: dict, client: "TAGOClient") -> "Station": nodeord = data.get("nodeord"), ) - @classmethod - def from_list(cls, data: list[dict], client: "TAGOClient") -> list["Station"]: - return [cls.from_dict(station, client) for station in data] + # @classmethod + # def from_list(cls, data: list[dict]) -> list["Station"]: + # return [cls.from_dict(station, client) for station in data] \ No newline at end of file From b131ed3b638c58a4b863d6794f9a681342ff1c19 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:50:36 +0900 Subject: [PATCH 09/27] =?UTF-8?q?chore=20:=20client=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 16 +++++++--------- tagoapi/utils/parser.py | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tagoapi/client.py b/tagoapi/client.py index 9b6c3ef..f80232d 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -27,7 +27,7 @@ def get_station(self, cityCode: int, nodeNo: int) -> list[Station]: ... @overload def get_station(self, cityCode: int, nodeNo: Optional[int], nodeNm: str) -> list[Station]: ... - @from_cache_or_fetch(604800) + @covert_model(604800) def get_route_by_no( self, cityCode: int, @@ -38,7 +38,7 @@ def get_route_by_no( params = build_params(self.auth, cityCode=cityCode, routeNo=routeNo) return self._fetch_and_convert(endpoint, params, Route) - @from_cache_or_fetch(604800) + @covert_model(604800) def get_route_by_id( self, cityCode: int, @@ -49,7 +49,7 @@ def get_route_by_id( params = build_params(self.auth, cityCode=cityCode, routeId=routeId) return self._fetch_and_convert(endpoint, params, Route, is_list=False) - @from_cache_or_fetch(604800) + @covert_model(604800) def get_route_by_station( self, cityCode: int, @@ -61,7 +61,7 @@ def get_route_by_station( return self._fetch_and_convert(endpoint, params, Route) - @from_cache_or_fetch(604800) + @covert_model(604800) def get_station_by_route( self, cityCode: int, @@ -72,15 +72,13 @@ def get_station_by_route( params= build_params(self.auth, cityCode=cityCode, routeId=routeId) return self._fetch_and_convert(endpoint, params, Station) - @from_cache_or_fetch(86400) + @covert_model(86400, Station) def get_station( self, cityCode: int, nodeNo: int = None, nodeNm: str = None, - _is_cache: bool = True, - _model: BaseModel = Station, - _is_list: bool = True + _is_cache: bool = True ) -> list[Station]: """정류소명 또는 번호로 정류소를 조회합니다""" if not (nodeNo or nodeNm): @@ -90,7 +88,7 @@ def get_station( params= build_params(self.auth, cityCode=cityCode, nodeNm=nodeNm,nodeNo=nodeNo) return self._fetch_and_convert(endpoint, params) - @from_cache_or_fetch(86400) + @covert_model(86400) def get_station_by_gps( self, gpsLati: float, diff --git a/tagoapi/utils/parser.py b/tagoapi/utils/parser.py index bd06611..87d0401 100644 --- a/tagoapi/utils/parser.py +++ b/tagoapi/utils/parser.py @@ -4,7 +4,7 @@ U = TypeVar('U', dict[str], list) # list, dict으로 반환할 때 -def parse_metadata(res: dict) -> U: +def parse_metadata(res: dict) -> None: striped = res.get("response", {}).get("body", {}).get("items", {}) if isinstance(striped, dict): return striped.get("item", None) From 6e4c20b98ab4a8f06fad73d7aad83a2c3ea65b2c Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:50:45 +0900 Subject: [PATCH 10/27] =?UTF-8?q?test:=20client=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_client.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/test_client.py diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..78dda83 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,37 @@ +import unittest +from unittest.mock import patch +from tagoapi import TAGOClient, TAGOAuth +from tagoapi.models import Station + +class TestTagoClient(unittest.TestCase): + def setUp(self): + self.client = TAGOClient(auth=TAGOAuth("dummy_api_key")) + + @patch.object(TAGOClient, "_get") + def test_get_station_return_models(self, mock_get): + # _get이 반환할 가짜 객체 생성 + fake_response = { + "response": { + "body": { + "items": { + "item":[ + {'gpslati': 35.86615, 'gpslong': 128.60002, 'nodeid': 'DGB7001009400', 'nodenm': '삼덕교회', + 'nodeno': 20075}, + {'gpslati': 35.86615, 'gpslong': 128.60002, 'nodeid': 'DGB7001009400', 'nodenm': '삼덕교회', + 'nodeno': 20075} + ] + } + } + } + } + mock_get.return_value = fake_response + + # 실행 + stations = self.client.get_station(cityCode=25, nodeNm="엄랭") + + + print(stations[0]._client) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 2f27f46baf37879bb62c81e4221c1272ef513a67 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:48:02 +0900 Subject: [PATCH 11/27] =?UTF-8?q?chore:=20dictionary=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 2 +- tagoapi/utils/cache_util.py | 8 ++++---- tests/test_client.py | 19 +++++++++++++++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tagoapi/client.py b/tagoapi/client.py index f80232d..42595e2 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -78,7 +78,7 @@ def get_station( cityCode: int, nodeNo: int = None, nodeNm: str = None, - _is_cache: bool = True + _is_cached: bool = True ) -> list[Station]: """정류소명 또는 번호로 정류소를 조회합니다""" if not (nodeNo or nodeNm): diff --git a/tagoapi/utils/cache_util.py b/tagoapi/utils/cache_util.py index 3876244..59ad8bd 100644 --- a/tagoapi/utils/cache_util.py +++ b/tagoapi/utils/cache_util.py @@ -28,12 +28,12 @@ def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아 if model: ## convert list if isinstance(raw, list): - res = model.from_list(cached) + res = model.from_list(raw) else: - res = model.from_dict(cached) + res = model.from_dict(raw) + # print(is_list and not isinstance(res, BaseList)) res.set_client(self) - - return res if is_list and not isinstance(res, BaseList) else BaseList(res) + return BaseList([res]) if is_list and not isinstance(res, BaseList) else res return raw diff --git a/tests/test_client.py b/tests/test_client.py index 78dda83..0a9f1f2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,7 +1,6 @@ import unittest from unittest.mock import patch from tagoapi import TAGOClient, TAGOAuth -from tagoapi.models import Station class TestTagoClient(unittest.TestCase): def setUp(self): @@ -30,7 +29,23 @@ def test_get_station_return_models(self, mock_get): stations = self.client.get_station(cityCode=25, nodeNm="엄랭") - print(stations[0]._client) + print(stations) + + @patch.object(TAGOClient, "_get") + def test_get_station_with_dict(self, mock_get): + fake_response = { "response": { "body": { + "items": { + "item": {'gpslati': 35.86615, 'gpslong': 128.60002, 'nodeid': 'DGB7001009400', 'nodenm': '삼덕교회', + 'nodeno': 20075} + } + }}} + mock_get.return_value = fake_response + + stations = self.client.get_station(cityCode=543, nodeNm="엄랭1") + + + print(stations) + if __name__ == "__main__": From 67a1193076bef236089751d1e9235cc3288c2fe6 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:11:23 +0900 Subject: [PATCH 12/27] =?UTF-8?q?refactor:=20model=5Fconvert=20=EB=8D=B0?= =?UTF-8?q?=EC=BD=94=EB=A0=88=EC=9D=B4=ED=84=B0=20cache=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=97=AC=EB=B6=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 7 +++---- tagoapi/utils/cache_util.py | 5 ++--- tests/test_client.py | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/tagoapi/client.py b/tagoapi/client.py index 42595e2..9edb0a7 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -78,7 +78,6 @@ def get_station( cityCode: int, nodeNo: int = None, nodeNm: str = None, - _is_cached: bool = True ) -> list[Station]: """정류소명 또는 번호로 정류소를 조회합니다""" if not (nodeNo or nodeNm): @@ -88,16 +87,16 @@ def get_station( params= build_params(self.auth, cityCode=cityCode, nodeNm=nodeNm,nodeNo=nodeNo) return self._fetch_and_convert(endpoint, params) - @covert_model(86400) + @covert_model(86400, Station, is_cached=False) def get_station_by_gps( self, gpsLati: float, - gpsLong: float + gpsLong: float, ) -> list[Station]: """GPS 좌표 기반으로 주변 정류소를 조회합니다""" endpoint = f'{self.BUSTATION}/getCrdntPrxmtSttnList' params = build_params(self.auth, gpsLati=gpsLati, gpsLong=gpsLong) - return self._fetch_and_convert(endpoint, params, Station) + return self._fetch_and_convert(endpoint, params) def get_arrival_by_station( diff --git a/tagoapi/utils/cache_util.py b/tagoapi/utils/cache_util.py index 59ad8bd..3eba191 100644 --- a/tagoapi/utils/cache_util.py +++ b/tagoapi/utils/cache_util.py @@ -1,3 +1,4 @@ +from functools import wraps from typing import TYPE_CHECKING from .cache import Cache from ..models.BaseList import BaseList @@ -9,11 +10,9 @@ cache = Cache() # method에서만 사용할 함수 -def covert_model(ttl: int = 86400, model: type["BaseModel"] = None, is_list: bool = True): # 데코레이터가 사용할 매개변수 +def covert_model(ttl: int = 86400, model: type["BaseModel"] = None, is_cached: bool = True, is_list: bool = True): # 데코레이터가 사용할 매개변수 def decorator(fn): # 호출할 함수를 매개변수로 받음 def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아서 이를 실행 - is_cached = kwargs.pop("_is_cached", True) - key = _make_cache_key(*args, _fname=fn.__name__, **kwargs) if is_cached else None cached = cache.get(key) if key else None diff --git a/tests/test_client.py b/tests/test_client.py index 0a9f1f2..85ed987 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,6 +1,6 @@ import unittest from unittest.mock import patch -from tagoapi import TAGOClient, TAGOAuth +from tagoapi import TAGOClient, TAGOAuth, cache class TestTagoClient(unittest.TestCase): def setUp(self): @@ -43,9 +43,22 @@ def test_get_station_with_dict(self, mock_get): stations = self.client.get_station(cityCode=543, nodeNm="엄랭1") - + print(cache.current_cache) print(stations) + @patch.object(TAGOClient, "_get") + def test_get_station_no_cache(self, mock_get): + fake_response = {"response": {"body": { + "items": { + "item": {'gpslati': 35.86615, 'gpslong': 128.60002, 'nodeid': 'DGB1234123423', 'nodenm': '이거보세요!!', + 'nodeno': 20075} + } + }}} + mock_get.return_value = fake_response + + stations = self.client.get_station_by_gps(gpsLati=1, gpsLong=3.425) + + print(stations) if __name__ == "__main__": From f025f0e8ad648886a58e9d73173fbe856d80c241 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:21:37 +0900 Subject: [PATCH 13/27] =?UTF-8?q?chore:=20Model=EC=97=90=EC=84=9C=20from?= =?UTF-8?q?=5Flist=20=EC=98=A4=EB=B2=84=EB=9D=BC=EC=9D=B4=EB=94=A9=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EB=8D=B0=EC=BD=94=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95(co?= =?UTF-8?q?nvert=5Fmodel),=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 36 ++++++++++++++++++----------------- tagoapi/models/ArrivalInfo.py | 6 +++--- tagoapi/models/Route.py | 6 +++--- tagoapi/models/Vehicle.py | 6 +++--- tests/test_client.py | 14 ++++++++++++++ 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/tagoapi/client.py b/tagoapi/client.py index 9edb0a7..e2529bb 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -27,7 +27,7 @@ def get_station(self, cityCode: int, nodeNo: int) -> list[Station]: ... @overload def get_station(self, cityCode: int, nodeNo: Optional[int], nodeNm: str) -> list[Station]: ... - @covert_model(604800) + @convert_model(604800, Route) def get_route_by_no( self, cityCode: int, @@ -36,9 +36,9 @@ def get_route_by_no( """노선 번호로 버스를 조회합니다""" endpoint = f'{self.BUSROUTE}/getRouteNoList' params = build_params(self.auth, cityCode=cityCode, routeNo=routeNo) - return self._fetch_and_convert(endpoint, params, Route) + return self._fetch_and_convert(endpoint, params) - @covert_model(604800) + @convert_model(604800, Route, is_list=False) def get_route_by_id( self, cityCode: int, @@ -47,9 +47,9 @@ def get_route_by_id( """노선 ID로 버스 정보를 조회합니다""" endpoint = f'{self.BUSROUTE}/getRouteInfoIem' params = build_params(self.auth, cityCode=cityCode, routeId=routeId) - return self._fetch_and_convert(endpoint, params, Route, is_list=False) + return self._fetch_and_convert(endpoint, params) - @covert_model(604800) + @convert_model(604800, Route) def get_route_by_station( self, cityCode: int, @@ -58,10 +58,10 @@ def get_route_by_station( """정류소를 경유하는 노선을 조회합니다""" endpoint = f'{self.BUSTATION}/getSttnThrghRouteList' params = build_params(self.auth, cityCode=cityCode, nodeid=nodeId) - return self._fetch_and_convert(endpoint, params, Route) + return self._fetch_and_convert(endpoint, params) - @covert_model(604800) + @convert_model(604800, Station) def get_station_by_route( self, cityCode: int, @@ -70,9 +70,9 @@ def get_station_by_route( """노선이 경유하는 정류소를 조회합니다""" endpoint = f'{self.BUSROUTE}/getRouteAcctoThrghSttnList' params= build_params(self.auth, cityCode=cityCode, routeId=routeId) - return self._fetch_and_convert(endpoint, params, Station) + return self._fetch_and_convert(endpoint, params) - @covert_model(86400, Station) + @convert_model(86400, Station) def get_station( self, cityCode: int, @@ -87,7 +87,7 @@ def get_station( params= build_params(self.auth, cityCode=cityCode, nodeNm=nodeNm,nodeNo=nodeNo) return self._fetch_and_convert(endpoint, params) - @covert_model(86400, Station, is_cached=False) + @convert_model(86400, Station, is_cached=False) def get_station_by_gps( self, gpsLati: float, @@ -98,7 +98,7 @@ def get_station_by_gps( params = build_params(self.auth, gpsLati=gpsLati, gpsLong=gpsLong) return self._fetch_and_convert(endpoint, params) - + @convert_model(model=ArrivalInfo, is_cached=False) def get_arrival_by_station( self, cityCode: int, @@ -107,8 +107,9 @@ def get_arrival_by_station( """실시간 도착예정정보 및 운행정보 목록을 조회합니다""" endpoint = f'{self.AVRINFO}/getSttnAcctoArvlPrearngeInfoList' params = build_params(self.auth, cityCode=cityCode, nodeId=nodeId) - return self._fetch_and_convert(endpoint, params, ArrivalInfo) - + return self._fetch_and_convert(endpoint, params) + + @convert_model(model=ArrivalInfo, is_cached=False) def get_route_arrival_by_station( self, cityCode: int, @@ -118,9 +119,9 @@ def get_route_arrival_by_station( """특정노선의 실시간 도착예정정보 및 운행정보 목록을 조회합니다""" endpoint = f'{self.AVRINFO}/getSttnAcctoSpcifyRouteBusArvlPrearngeInfoList' params = build_params(self.auth, cityCode=cityCode, nodeId=nodeId, routeId=routeId) - return self._fetch_and_convert(endpoint, params, ArrivalInfo) + return self._fetch_and_convert(endpoint, params) - + @convert_model(model=Vehicle, is_cached=False) def get_route_pos( self, cityCode: int, @@ -129,8 +130,9 @@ def get_route_pos( """버스의 S위치정보의 목록을 조회합니다""" endpoint = f'{self.BUSPOS}/getRouteAcctoBusLcList' params = build_params(self.auth, cityCode=cityCode, routeId=routeId) - return self._fetch_and_convert(endpoint, params, Vehicle, is_cache=False) + return self._fetch_and_convert(endpoint, params) + @convert_model(model=Vehicle, is_cached=False) def get_route_pos_near_station( self, cityCode: int, @@ -140,7 +142,7 @@ def get_route_pos_near_station( """특정정류소에 접근한 버스의 위치정보를 조회합니다""" endpoint = f'{self.BUSPOS}/getRouteAcctoSpcifySttnAccesBusLcInfo' params = build_params(self.auth, cityCode=cityCode, routeId=routeId, nodeId=nodeId) - return self._fetch_and_convert(endpoint, params, Vehicle) + return self._fetch_and_convert(endpoint, params) def _fetch_and_convert( self, diff --git a/tagoapi/models/ArrivalInfo.py b/tagoapi/models/ArrivalInfo.py index 2b877ae..1f755f0 100644 --- a/tagoapi/models/ArrivalInfo.py +++ b/tagoapi/models/ArrivalInfo.py @@ -40,6 +40,6 @@ def from_dict(cls, data: dict) -> "ArrivalInfo": arrtime = data.get("arrtime") ) - @classmethod - def from_list(cls, data: list[dict]) -> list["ArrivalInfo"]: - return [cls.from_dict(station) for station in data] + # @classmethod + # def from_list(cls, data: list[dict]) -> list["ArrivalInfo"]: + # return [cls.from_dict(station) for station in data] diff --git a/tagoapi/models/Route.py b/tagoapi/models/Route.py index 688c85f..e3daf2f 100644 --- a/tagoapi/models/Route.py +++ b/tagoapi/models/Route.py @@ -48,6 +48,6 @@ def from_dict(cls, data: dict) -> "Route": startvehicletime=data.get("startvehicletime") ) - @classmethod - def from_list(cls, data: list[dict]) -> list["Route"]: - return [ cls.from_dict(route) for route in data ] + # @classmethod + # def from_list(cls, data: list[dict]) -> list["Route"]: + # return [ cls.from_dict(route) for route in data ] diff --git a/tagoapi/models/Vehicle.py b/tagoapi/models/Vehicle.py index 185a7ef..93306d9 100644 --- a/tagoapi/models/Vehicle.py +++ b/tagoapi/models/Vehicle.py @@ -44,6 +44,6 @@ def from_dict(cls, data: dict) -> "Vehicle": vehicleNo=data.get("vehicleno") ) - @classmethod - def from_list(cls, data: list[dict]) -> list["Vehicle"]: - return [ cls.from_dict(vehicle) for vehicle in data] \ No newline at end of file + # @classmethod + # def from_list(cls, data: list[dict]) -> list["Vehicle"]: + # return [ cls.from_dict(vehicle) for vehicle in data] \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index 85ed987..4849446 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -60,6 +60,20 @@ def test_get_station_no_cache(self, mock_get): print(stations) + @patch.object(TAGOClient, "_get") + def test_get_station_once(self, mock_get): + fake_response = {"response": {"body": { + "items": { + "item": {'routeid': "DGB573493541", 'routeno': 356, 'routetp': '123', 'nodenm': '이거보세요!!', + 'nodeno': 20075} + } + }}} + mock_get.return_value = fake_response + + route = self.client.get_route_by_id(cityCode=25, routeId="DGB573493541") + + print(route._client) + if __name__ == "__main__": unittest.main() \ No newline at end of file From 5def686971fb0ca910b7f0cbe46e7e03fd3fba74 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:22:48 +0900 Subject: [PATCH 14/27] =?UTF-8?q?chore:=20Model=EC=97=90=EC=84=9C=20from?= =?UTF-8?q?=5Flist=20=EC=98=A4=EB=B2=84=EB=9D=BC=EC=9D=B4=EB=94=A9=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EB=8D=B0=EC=BD=94=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95(co?= =?UTF-8?q?nvert=5Fmodel),=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/__init__.py | 4 ++-- tagoapi/utils/__init__.py | 4 ++-- tagoapi/utils/cache_util.py | 2 +- tests/test_client.py | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tagoapi/__init__.py b/tagoapi/__init__.py index 46ccbf3..68d9c50 100644 --- a/tagoapi/__init__.py +++ b/tagoapi/__init__.py @@ -8,11 +8,11 @@ from .models import Station # from .utils import get_city_code -from .utils.cache_util import covert_model +from .utils.cache_util import convert_model from .utils.parser import KeyExtract from .utils.cache_util import cache from .utils.get_station import get_station -__all__ = [ 'TAGOClient', 'TAGOAuth', 'covert_model', 'Route', 'Vehicle', 'Station', 'KeyExtract', 'cache', 'get_station' ] \ No newline at end of file +__all__ = [ 'TAGOClient', 'TAGOAuth', 'convert_model', 'Route', 'Vehicle', 'Station', 'KeyExtract', 'cache', 'get_station' ] \ No newline at end of file diff --git a/tagoapi/utils/__init__.py b/tagoapi/utils/__init__.py index 65ab6a6..a21262f 100644 --- a/tagoapi/utils/__init__.py +++ b/tagoapi/utils/__init__.py @@ -1,5 +1,5 @@ from .cache import Cache -from .cache_util import covert_model +from .cache_util import convert_model from .cache_util import cache from .parser import parse_metadata from .convertor import convert @@ -13,7 +13,7 @@ __all__ = [ "Cache", "KeyExtract", - "covert_model", + "convert_model", "parse_metadata", "convert", "build_params", diff --git a/tagoapi/utils/cache_util.py b/tagoapi/utils/cache_util.py index 3eba191..ade0b2e 100644 --- a/tagoapi/utils/cache_util.py +++ b/tagoapi/utils/cache_util.py @@ -10,7 +10,7 @@ cache = Cache() # method에서만 사용할 함수 -def covert_model(ttl: int = 86400, model: type["BaseModel"] = None, is_cached: bool = True, is_list: bool = True): # 데코레이터가 사용할 매개변수 +def convert_model(ttl: int = 86400, model: type["BaseModel"] = None, is_cached: bool = True, is_list: bool = True): # 데코레이터가 사용할 매개변수 def decorator(fn): # 호출할 함수를 매개변수로 받음 def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아서 이를 실행 key = _make_cache_key(*args, _fname=fn.__name__, **kwargs) if is_cached else None diff --git a/tests/test_client.py b/tests/test_client.py index 4849446..f6c2a53 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -73,6 +73,7 @@ def test_get_station_once(self, mock_get): route = self.client.get_route_by_id(cityCode=25, routeId="DGB573493541") print(route._client) + print(cache.current_cache) if __name__ == "__main__": From f46eeea53a6798d4cf5857fb3676cd222af73ad4 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:51:35 +0900 Subject: [PATCH 15/27] =?UTF-8?q?feat:=20BaseModel=20LazyLoading=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 7 ++-- tagoapi/models/BaseModel.py | 37 ++++++++++++++++++++- tagoapi/models/Route.py | 9 +++++- tests/test_object_route.py | 64 +++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 tests/test_object_route.py diff --git a/tagoapi/client.py b/tagoapi/client.py index e2529bb..5c40e23 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -153,8 +153,11 @@ def _fetch_and_convert( return response - - ############################# + ######## method for LazyLoading ################ + def _get_route(self, + cityCode: int, + routeId: str) -> Route: + return self.get_route_by_id(cityCode, routeId) def _get(self, endpoint: str, params: dict) -> any: diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index 8a91b97..5ddd32a 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -5,6 +5,8 @@ class BaseModel: cache_key = "BaseModel:" + _lazy_fields: dict = {} + def __init__(self): self._client = None @@ -26,5 +28,38 @@ def from_list(cls, data: list) -> BaseList: # # return model - + def __getattr__(self, item): + + if item in self._lazy_fields: + if self._client is None: + raise RuntimeError(f"{self.__class__.__name__} cannot be loaded without client") + + _client = self._client + loader = getattr(_client, self._lazy_fields[item]) + loaded_value = loader(self) + + # 같은 개체라면 | 속성 저장 + if isinstance(loaded_value, self.__class__): + for k, v in loaded_value.to_dict().items(): + + if not k.startswith("_"): + setattr(self, k, v) + else: # 다른 개체라면 item = loaded_value + setattr(self, item, loaded_value) + + return self.__dict__[item] + + + raise AttributeError(f"{self.__class__.__name__} has no attribute {item}") + + + ## 모든 속성에 접근할 때 가장 먼저 호출되는 메서드 + ## 구현할 때 없으면 raise 땡기고, 거기서 만약 인스턴스 두개가 다르면 그거 자체를 속성으로 저장하고 같으면 속성을 하나씩 저장하게 + def __getattribute__(self, item): + if super().__getattribute__(item) is not None: + return super().__getattribute__(item) + if super().__getattribute__("_client") is None: + raise RuntimeError(f"{self.__class__.__name__} cannot be loaded without client") + + raise AttributeError(f"{self.__class__.__name__} object has no attribute {item}") diff --git a/tagoapi/models/Route.py b/tagoapi/models/Route.py index e3daf2f..e2f5762 100644 --- a/tagoapi/models/Route.py +++ b/tagoapi/models/Route.py @@ -8,6 +8,7 @@ # 버스 노선 자체에 관한 정보 class Route(BaseModel): cache_key = "Route:" + _lazy_fields = {"endvehicletime": "get_route_by_id"} def __init__( self, @@ -18,6 +19,8 @@ def __init__( startNodeNm: str = None, endvehicletime: int = None, startvehicletime: int = None, + intervalsattime: int = None, + intervalsuntime: int = None #TODO: 정류장 리스트도 넣으면 좋을 듯 합니당 ): super().__init__() @@ -29,6 +32,8 @@ def __init__( self.startNodeNm = startNodeNm self.endvehicletime = endvehicletime self.startvehicletime = startvehicletime + self.intervalsattime = intervalsattime + self.intervalsuntime = intervalsuntime def __repr__(self): return f"Route({self.routeNo})" @@ -45,7 +50,9 @@ def from_dict(cls, data: dict) -> "Route": startNodeNm=data.get("startnodenm"), endNodeNm=data.get("endnodenm"), endvehicletime=data.get("endvehicletime"), - startvehicletime=data.get("startvehicletime") + startvehicletime=data.get("startvehicletime"), + intervalsattime=data.get("intervalsattime"), + intervalsuntime=data.get("intervalsuntime") ) # @classmethod diff --git a/tests/test_object_route.py b/tests/test_object_route.py new file mode 100644 index 0000000..f249db5 --- /dev/null +++ b/tests/test_object_route.py @@ -0,0 +1,64 @@ +import unittest +from unittest.mock import MagicMock + +from tagoapi.models import Route, Station +from tagoapi.models.BaseList import BaseList + +class TestRoute(unittest.TestCase): + def setUp(self): + self.route = Route("453", routeNo="북구1") + def test_attributes(self): ## 속성 테스트 + print("\n====== test attributes ======") + + print("route:", self.route) + + def test_AttributeError(self): ## 없는 속성 테스트 + print("\n====== test AttributeError ======") + + with self.assertRaises(AttributeError): + print("name:", self.route.name) + + def test_RuntimeError(self): ## client 주입 테스트 + print("\n====== test RuntimeError ======") + + with self.assertRaises(RuntimeError): + print("endtime:", self.route.endvehicletime) + + def test_lazy_load_in_class(self): ## lazy_load ( attribute in class ) + print("\n====== test lazy_load ( attribute in class ) ======") + + route = Route("453", routeNo="북구4") + + # client_mock 생성 + mock_client = MagicMock() + mock_client.get_route_by_id.return_value = Route("453", routeNo="북구4", endvehicletime=53) + route.set_client(mock_client) + + print(route.routeNo) + + print(route.endvehicletime) + + def test_lazy_load_not_in_class(self): ## lazy_load ( attribute not in class ) + print("\n====== test lazy_load ( attribute not in class ) ======") + + route = Route("564", routeNo="북구2") + + mock_client = MagicMock() + mock_client.get_stations.return_value = BaseList([ + Station("안녕하시귀", "이런다"), + Station("안녕하시귀", "이런다1"), + Station("안녕하시귀", "이런다2"), + Station("안녕하시귀", "이런다3") + ]) + + route.set_client(mock_client) + route._lazy_fields = {"stations": "get_stations"} + + print(route.routeNo) + print(route.stations) + + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 0c8552ef4976e0b467bef751b8d697837bcb0e05 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:12:12 +0900 Subject: [PATCH 16/27] =?UTF-8?q?feat:=20BaseModel=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EC=97=90=20cityCode=20=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 8 ++++---- tagoapi/models/ArrivalInfo.py | 2 ++ tagoapi/models/BaseModel.py | 3 ++- tagoapi/models/Route.py | 12 ++++++++++-- tagoapi/models/Station.py | 2 +- tagoapi/models/Vehicle.py | 4 ++-- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tagoapi/client.py b/tagoapi/client.py index 5c40e23..b019374 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -154,10 +154,10 @@ def _fetch_and_convert( ######## method for LazyLoading ################ - def _get_route(self, - cityCode: int, - routeId: str) -> Route: - return self.get_route_by_id(cityCode, routeId) + def _get_route(self, route: Route) -> Route: + return self.get_route_by_id(route.cityCode, route.routeId) + + def _get_stations_by_route(self,): def _get(self, endpoint: str, params: dict) -> any: diff --git a/tagoapi/models/ArrivalInfo.py b/tagoapi/models/ArrivalInfo.py index 1f755f0..daeecfd 100644 --- a/tagoapi/models/ArrivalInfo.py +++ b/tagoapi/models/ArrivalInfo.py @@ -5,6 +5,7 @@ class ArrivalInfo(BaseModel): def __init__(self, nodeId, nodeNm, + cityCode, routeId: str, routeNo: str, routeTp, @@ -12,6 +13,7 @@ def __init__(self, vehicleTp: str = None, arrtime: int = None ): + super().__init__(cityCode) self.nodeId = nodeId self.nodeNm = nodeNm self.routeId = routeId diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index 5ddd32a..60de846 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -7,8 +7,9 @@ class BaseModel: cache_key = "BaseModel:" _lazy_fields: dict = {} - def __init__(self): + def __init__(self, cityCode: int): self._client = None + self.cityCode = cityCode def to_dict(self) -> dict: return vars(self) diff --git a/tagoapi/models/Route.py b/tagoapi/models/Route.py index e2f5762..fa08a7f 100644 --- a/tagoapi/models/Route.py +++ b/tagoapi/models/Route.py @@ -8,11 +8,19 @@ # 버스 노선 자체에 관한 정보 class Route(BaseModel): cache_key = "Route:" - _lazy_fields = {"endvehicletime": "get_route_by_id"} + _lazy_fields = { + "endvehicletime": "_get_route", + "startvehicletime": "_get_route", + "intervaltime": "_get_route", + "intervalsattime": "_get_route", + "intervalsuntime": "_get_route", + "stations": "_get_stations_by_route" + } def __init__( self, routeId: str, + cityCode: int, routeNo: str = None, routeTp: str = None, endNodeNm: str = None, @@ -23,7 +31,7 @@ def __init__( intervalsuntime: int = None #TODO: 정류장 리스트도 넣으면 좋을 듯 합니당 ): - super().__init__() + super().__init__(cityCode) self.routeId = routeId self.routeNo = routeNo diff --git a/tagoapi/models/Station.py b/tagoapi/models/Station.py index 670d952..730de7b 100644 --- a/tagoapi/models/Station.py +++ b/tagoapi/models/Station.py @@ -20,7 +20,7 @@ def __init__( nodeord: int = None, # *routeList: list['Route'] ): - super().__init__() + super().__init__(cityCode) self.nodeId = nodeId self.nodeNm = nodeNm self.nodeNo = nodeNo diff --git a/tagoapi/models/Vehicle.py b/tagoapi/models/Vehicle.py index 93306d9..49a8e92 100644 --- a/tagoapi/models/Vehicle.py +++ b/tagoapi/models/Vehicle.py @@ -4,7 +4,7 @@ class Vehicle(BaseModel): def __init__( self, - route: Route = None, + cityCode: int, routeId: str = None, routeNo: str = None, gpsLati: float = None, @@ -14,7 +14,7 @@ def __init__( vehicleTp: str = None, vehicleNo: str = None ): - self.route = route + super().__init__(cityCode) self.routeId = routeId self.routeNo = routeNo self.gpsLati = gpsLati From 0674d68f8108bae354f2fe4ffe7d5e37041fdcf0 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:18:19 +0900 Subject: [PATCH 17/27] =?UTF-8?q?chore:=20client=20=5Fget=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=8B=9C=20cityCode=EB=8F=84=20=ED=95=A8=EA=BB=98=20c?= =?UTF-8?q?onvert=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 24 +++++++++++++----------- tagoapi/models/Route.py | 1 + tagoapi/utils/parser.py | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tagoapi/client.py b/tagoapi/client.py index b019374..b541a0c 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -36,7 +36,7 @@ def get_route_by_no( """노선 번호로 버스를 조회합니다""" endpoint = f'{self.BUSROUTE}/getRouteNoList' params = build_params(self.auth, cityCode=cityCode, routeNo=routeNo) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(604800, Route, is_list=False) def get_route_by_id( @@ -47,7 +47,7 @@ def get_route_by_id( """노선 ID로 버스 정보를 조회합니다""" endpoint = f'{self.BUSROUTE}/getRouteInfoIem' params = build_params(self.auth, cityCode=cityCode, routeId=routeId) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(604800, Route) def get_route_by_station( @@ -58,7 +58,7 @@ def get_route_by_station( """정류소를 경유하는 노선을 조회합니다""" endpoint = f'{self.BUSTATION}/getSttnThrghRouteList' params = build_params(self.auth, cityCode=cityCode, nodeid=nodeId) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(604800, Station) @@ -70,7 +70,7 @@ def get_station_by_route( """노선이 경유하는 정류소를 조회합니다""" endpoint = f'{self.BUSROUTE}/getRouteAcctoThrghSttnList' params= build_params(self.auth, cityCode=cityCode, routeId=routeId) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(86400, Station) def get_station( @@ -85,7 +85,7 @@ def get_station( endpoint = f'{self.BUSTATION}/getSttnNoList' params= build_params(self.auth, cityCode=cityCode, nodeNm=nodeNm,nodeNo=nodeNo) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(86400, Station, is_cached=False) def get_station_by_gps( @@ -107,7 +107,7 @@ def get_arrival_by_station( """실시간 도착예정정보 및 운행정보 목록을 조회합니다""" endpoint = f'{self.AVRINFO}/getSttnAcctoArvlPrearngeInfoList' params = build_params(self.auth, cityCode=cityCode, nodeId=nodeId) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(model=ArrivalInfo, is_cached=False) def get_route_arrival_by_station( @@ -119,7 +119,7 @@ def get_route_arrival_by_station( """특정노선의 실시간 도착예정정보 및 운행정보 목록을 조회합니다""" endpoint = f'{self.AVRINFO}/getSttnAcctoSpcifyRouteBusArvlPrearngeInfoList' params = build_params(self.auth, cityCode=cityCode, nodeId=nodeId, routeId=routeId) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(model=Vehicle, is_cached=False) def get_route_pos( @@ -130,7 +130,7 @@ def get_route_pos( """버스의 S위치정보의 목록을 조회합니다""" endpoint = f'{self.BUSPOS}/getRouteAcctoBusLcList' params = build_params(self.auth, cityCode=cityCode, routeId=routeId) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) @convert_model(model=Vehicle, is_cached=False) def get_route_pos_near_station( @@ -142,22 +142,24 @@ def get_route_pos_near_station( """특정정류소에 접근한 버스의 위치정보를 조회합니다""" endpoint = f'{self.BUSPOS}/getRouteAcctoSpcifySttnAccesBusLcInfo' params = build_params(self.auth, cityCode=cityCode, routeId=routeId, nodeId=nodeId) - return self._fetch_and_convert(endpoint, params) + return self._fetch_and_convert(endpoint, params, citycode=cityCode) def _fetch_and_convert( self, endpoint: str, params: dict, + **kwargs ) -> list | dict: response = parse_metadata(self._get(endpoint, params)) - return response + return {**response, **kwargs} ######## method for LazyLoading ################ def _get_route(self, route: Route) -> Route: return self.get_route_by_id(route.cityCode, route.routeId) - def _get_stations_by_route(self,): + def _get_stations_by_route(self, route: Route) -> list[Station]: + return self.get_station_by_route(route.cityCode, route.routeId) def _get(self, endpoint: str, params: dict) -> any: diff --git a/tagoapi/models/Route.py b/tagoapi/models/Route.py index fa08a7f..dcfb31f 100644 --- a/tagoapi/models/Route.py +++ b/tagoapi/models/Route.py @@ -53,6 +53,7 @@ def to_dict(self): def from_dict(cls, data: dict) -> "Route": return cls( routeId=data.get("routeid"), + cityCode=data.get("citycode"), routeNo=data.get("routeno"), routeTp=data.get("routetp"), startNodeNm=data.get("startnodenm"), diff --git a/tagoapi/utils/parser.py b/tagoapi/utils/parser.py index 87d0401..d0bc0ad 100644 --- a/tagoapi/utils/parser.py +++ b/tagoapi/utils/parser.py @@ -4,7 +4,7 @@ U = TypeVar('U', dict[str], list) # list, dict으로 반환할 때 -def parse_metadata(res: dict) -> None: +def parse_metadata(res: dict) -> U | None: striped = res.get("response", {}).get("body", {}).get("items", {}) if isinstance(striped, dict): return striped.get("item", None) From 76f1a5f9e0a7f79e31c3baa08cc4d125ce31e3f7 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Tue, 30 Sep 2025 08:34:34 +0900 Subject: [PATCH 18/27] feat: set Lazy_fields in Station --- tagoapi/client.py | 26 +++++++++++++++++--------- tagoapi/models/Station.py | 7 +++---- tests/test_object_route.py | 8 ++++---- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/tagoapi/client.py b/tagoapi/client.py index b541a0c..50c6f69 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -144,6 +144,23 @@ def get_route_pos_near_station( params = build_params(self.auth, cityCode=cityCode, routeId=routeId, nodeId=nodeId) return self._fetch_and_convert(endpoint, params, citycode=cityCode) + + ######## method for LazyLoading ################ + + + def _get_route(self, route: Route) -> Route: + return self.get_route_by_id(route.cityCode, route.routeId) + + def _get_stations_by_route(self, route: Route) -> list[Station]: + return self.get_station_by_route(route.cityCode, route.routeId) + + def _get_routes_by_station(self, station: Station) -> list[Route]: + return self.get_route_by_station(station.cityCode, station.nodeId) + + + ######## get util ################ + + def _fetch_and_convert( self, endpoint: str, @@ -153,15 +170,6 @@ def _fetch_and_convert( response = parse_metadata(self._get(endpoint, params)) return {**response, **kwargs} - - ######## method for LazyLoading ################ - def _get_route(self, route: Route) -> Route: - return self.get_route_by_id(route.cityCode, route.routeId) - - def _get_stations_by_route(self, route: Route) -> list[Station]: - return self.get_station_by_route(route.cityCode, route.routeId) - - def _get(self, endpoint: str, params: dict) -> any: response = http_get(f"{self.BASE_URL}/{endpoint}", params=params) error_code = response.get("returnReasonCode") diff --git a/tagoapi/models/Station.py b/tagoapi/models/Station.py index 730de7b..a2040cc 100644 --- a/tagoapi/models/Station.py +++ b/tagoapi/models/Station.py @@ -1,12 +1,12 @@ from typing import TYPE_CHECKING from .BaseModel import BaseModel -if TYPE_CHECKING: - from .Route import Route - from tagoapi import TAGOClient class Station(BaseModel): # cache_key = "Station:" cache_key = "Station:" + _lazy_fields = { + "routes": "_get_routes_by_station", + } def __init__( self, @@ -18,7 +18,6 @@ def __init__( cityCode: int = None, updowncd: int = None, nodeord: int = None, - # *routeList: list['Route'] ): super().__init__(cityCode) self.nodeId = nodeId diff --git a/tests/test_object_route.py b/tests/test_object_route.py index f249db5..1b37577 100644 --- a/tests/test_object_route.py +++ b/tests/test_object_route.py @@ -6,7 +6,7 @@ class TestRoute(unittest.TestCase): def setUp(self): - self.route = Route("453", routeNo="북구1") + self.route = Route("453", routeNo="북구1", cityCode=22) def test_attributes(self): ## 속성 테스트 print("\n====== test attributes ======") @@ -27,11 +27,11 @@ def test_RuntimeError(self): ## client 주입 테스트 def test_lazy_load_in_class(self): ## lazy_load ( attribute in class ) print("\n====== test lazy_load ( attribute in class ) ======") - route = Route("453", routeNo="북구4") + route = Route("453", routeNo="북구4", cityCode=22) # client_mock 생성 mock_client = MagicMock() - mock_client.get_route_by_id.return_value = Route("453", routeNo="북구4", endvehicletime=53) + mock_client.get_route_by_id.return_value = Route("453", routeNo="북구4", endvehicletime=53, cityCode=22) route.set_client(mock_client) print(route.routeNo) @@ -41,7 +41,7 @@ def test_lazy_load_in_class(self): ## lazy_load ( attribute in class ) def test_lazy_load_not_in_class(self): ## lazy_load ( attribute not in class ) print("\n====== test lazy_load ( attribute not in class ) ======") - route = Route("564", routeNo="북구2") + route = Route("564", routeNo="북구2", cityCode=22) mock_client = MagicMock() mock_client.get_stations.return_value = BaseList([ From 9ac35d7045444ce7a82d4545e8acc89f4a62ef56 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:19:57 +0900 Subject: [PATCH 19/27] feat: set Lazy_fields in ArrivalInfo and Vehicle --- tagoapi/client.py | 11 +++++++++++ tagoapi/models/ArrivalInfo.py | 5 +++++ tagoapi/models/Vehicle.py | 5 +++++ 3 files changed, 21 insertions(+) diff --git a/tagoapi/client.py b/tagoapi/client.py index 50c6f69..540b77e 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -157,6 +157,17 @@ def _get_stations_by_route(self, route: Route) -> list[Station]: def _get_routes_by_station(self, station: Station) -> list[Route]: return self.get_route_by_station(station.cityCode, station.nodeId) + def _get_station_by_arrival_info(self, arrivalInfo: ArrivalInfo) -> Station: + return self.get_station(arrivalInfo.cityCode, nodeNo=arrivalInfo.nodeNo)[0] + + def _get_route_by_arrival_info(self, arrivalInfo: ArrivalInfo) -> Route: + return self.get_route_by_id(arrivalInfo.cityCode, arrivalInfo.routeId) + + def _get_route_by_vehicle(self, vehicle: Vehicle) -> Route: + return self.get_route_by_id(vehicle.cityCode, vehicle.routeId) + + def _get_station_by_vehicle(self, vehicle: Vehicle) -> Station: + return self.get_station(vehicle.cityCode, nodeNo=vehicle.nodeNo)[0]; ######## get util ################ diff --git a/tagoapi/models/ArrivalInfo.py b/tagoapi/models/ArrivalInfo.py index daeecfd..692fe48 100644 --- a/tagoapi/models/ArrivalInfo.py +++ b/tagoapi/models/ArrivalInfo.py @@ -2,6 +2,11 @@ class ArrivalInfo(BaseModel): + _lazy_fields = { + "station": "_get_station_by_arrival_info", + "routes": "_get_route_by_arrival_info" + } + def __init__(self, nodeId, nodeNm, diff --git a/tagoapi/models/Vehicle.py b/tagoapi/models/Vehicle.py index 49a8e92..ddc4e88 100644 --- a/tagoapi/models/Vehicle.py +++ b/tagoapi/models/Vehicle.py @@ -2,6 +2,11 @@ from .BaseModel import BaseModel class Vehicle(BaseModel): + _lazy_fields = { + "route": '_get_route_by_vehicle', + "station": '_get_station_by_vehicle' + } + def __init__( self, cityCode: int, From acd454a8ae29840b2fb3fb0e677ca8c764010919 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:42:06 +0900 Subject: [PATCH 20/27] =?UTF-8?q?fix:=20=5Fclinet=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EC=8B=9C=20=5Fclient=EA=B0=80=20None?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=20RunTimeError=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/models/BaseList.py | 2 +- tagoapi/models/BaseModel.py | 2 ++ tests/test_object_route.py | 23 ++++++++++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/tagoapi/models/BaseList.py b/tagoapi/models/BaseList.py index 47be0a6..ccb7747 100644 --- a/tagoapi/models/BaseList.py +++ b/tagoapi/models/BaseList.py @@ -3,7 +3,7 @@ from tagoapi.models.BaseModel import BaseModel from tagoapi import TAGOClient -class BaseList(list): # list 상속 받아도 좋을듯 +class BaseList(list): def as_list(self) -> list["BaseModel"]: return list(self) diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index 60de846..78120d0 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -60,6 +60,8 @@ def __getattribute__(self, item): if super().__getattribute__(item) is not None: return super().__getattribute__(item) if super().__getattribute__("_client") is None: + if item == "_client": + return super().__getattribute__("_client") raise RuntimeError(f"{self.__class__.__name__} cannot be loaded without client") raise AttributeError(f"{self.__class__.__name__} object has no attribute {item}") diff --git a/tests/test_object_route.py b/tests/test_object_route.py index 1b37577..06022d9 100644 --- a/tests/test_object_route.py +++ b/tests/test_object_route.py @@ -31,15 +31,15 @@ def test_lazy_load_in_class(self): ## lazy_load ( attribute in class ) # client_mock 생성 mock_client = MagicMock() - mock_client.get_route_by_id.return_value = Route("453", routeNo="북구4", endvehicletime=53, cityCode=22) + mock_client._get_route.return_value = Route("453", routeNo="북구4", endvehicletime=53, cityCode=22) route.set_client(mock_client) print(route.routeNo) print(route.endvehicletime) - def test_lazy_load_not_in_class(self): ## lazy_load ( attribute not in class ) - print("\n====== test lazy_load ( attribute not in class ) ======") + def test_custom_lazy_load_not_in_class(self): ## custom lazy_load ( attribute not in class ) + print("\n====== test custom lazy_load ( attribute not in class ) ======") route = Route("564", routeNo="북구2", cityCode=22) @@ -57,6 +57,23 @@ def test_lazy_load_not_in_class(self): ## lazy_load ( attribute not in class ) print(route.routeNo) print(route.stations) + def test_lazy_load_not_in_class(self): ## lazy_load ( attribute not in class ) + print("\n====== test lazy_load ( attribute not in class ) ======") + + route = Route("564", routeNo="북구2", cityCode=22) + + mock_client = MagicMock() + mock_client._get_stations_by_route.return_value = BaseList([ + Station("안녕하시귀", "이런다"), + Station("안녕하시귀", "이런다1"), + Station("안녕하시귀", "이런다2"), + Station("안녕하시귀", "이런다3") + ]) + + route.set_client(mock_client) + + print(route.routeNo) + print(route.stations[0]._client) From 92269fea03b454b492f985d3421cd2c0c67cae7a Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 12 Oct 2025 08:21:12 +0900 Subject: [PATCH 21/27] =?UTF-8?q?feat:=20Station=20lazy=5Ffield=20?= =?UTF-8?q?=EC=9D=BC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/client.py | 3 +++ tagoapi/models/BaseList.py | 2 +- tagoapi/models/Station.py | 1 + tests/test_object_station.py | 45 ++++++++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/test_object_station.py diff --git a/tagoapi/client.py b/tagoapi/client.py index 540b77e..7bb35cd 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -154,6 +154,9 @@ def _get_route(self, route: Route) -> Route: def _get_stations_by_route(self, route: Route) -> list[Station]: return self.get_station_by_route(route.cityCode, route.routeId) + def _get_station(self, station: Station) -> Station: + return self.get_station(station.cityCode, nodeNm=station.nodeNm)[0] + def _get_routes_by_station(self, station: Station) -> list[Route]: return self.get_route_by_station(station.cityCode, station.nodeId) diff --git a/tagoapi/models/BaseList.py b/tagoapi/models/BaseList.py index ccb7747..40dd8dc 100644 --- a/tagoapi/models/BaseList.py +++ b/tagoapi/models/BaseList.py @@ -8,7 +8,7 @@ class BaseList(list): def as_list(self) -> list["BaseModel"]: return list(self) - def set_client(self, client: "TAGOClient"): + def set_client(self, client: "TAGOClient") -> "BaseList": for element in self: element.set_client(client) diff --git a/tagoapi/models/Station.py b/tagoapi/models/Station.py index a2040cc..8107551 100644 --- a/tagoapi/models/Station.py +++ b/tagoapi/models/Station.py @@ -6,6 +6,7 @@ class Station(BaseModel): cache_key = "Station:" _lazy_fields = { "routes": "_get_routes_by_station", + "nodeNo": "_get_station", # csv에서 nodeId로 찾을 수 있도록 수정 } def __init__( diff --git a/tests/test_object_station.py b/tests/test_object_station.py new file mode 100644 index 0000000..aff8e59 --- /dev/null +++ b/tests/test_object_station.py @@ -0,0 +1,45 @@ +import unittest +from unittest.mock import MagicMock + +from tagoapi.models import Route, Station +from tagoapi.models.BaseList import BaseList + +class TestRoute(unittest.TestCase): + def setUp(self): + self.station = Station("DGB573493541", "대구소프트웨어마이스터고등학교앞") + def test_attributes(self): ## 속성 테스트 + print("\n====== test attributes ======") + + print("station:", self.station) + + def test_AttributeError(self): ## 없는 속성 테스트 + print("\n====== test AttributeError ======") + + with self.assertRaises(AttributeError): + print("name:", self.station.name) + + def test_RuntimeError(self): ## client 주입 테스트 + print("\n====== test RuntimeError ======") + + with self.assertRaises(RuntimeError): + print("endtime:", self.station.routes) + + # def test_lazy_load_not_in_class(self): ## lazy_load ( attribute not in class ) + # print("\n====== test lazy_load ( attribute not in class ) ======") + # + # + # station = Station("DGB573493541", "대구소프트웨어마이스터고등학교앞") + # + # # client_mock 생성 + # mock_client = MagicMock() + # mock_client._get_station.return_value = Station("DGB573493541", "대구소프트웨어마이스터고등학교앞") + # station.set_client(mock_client) + # + # print(station.nodeNm) + # + # print(station.station) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From db377a576893b0c1ee423c3a9b04a199db41e733 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sun, 12 Oct 2025 08:26:24 +0900 Subject: [PATCH 22/27] =?UTF-8?q?chore:=20ArrivalInfo,=20Vehicle=20attribu?= =?UTF-8?q?te=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/models/ArrivalInfo.py | 25 ++++++++++--------------- tagoapi/models/Vehicle.py | 13 ++++++------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/tagoapi/models/ArrivalInfo.py b/tagoapi/models/ArrivalInfo.py index 692fe48..8ac5c66 100644 --- a/tagoapi/models/ArrivalInfo.py +++ b/tagoapi/models/ArrivalInfo.py @@ -1,4 +1,8 @@ from .BaseModel import BaseModel +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .Station import Station + from .Route import Route class ArrivalInfo(BaseModel): @@ -8,22 +12,16 @@ class ArrivalInfo(BaseModel): } def __init__(self, - nodeId, - nodeNm, + node: Station, + route: Route, cityCode, - routeId: str, - routeNo: str, - routeTp, arrprevstationcnt: int = None, vehicleTp: str = None, arrtime: int = None ): super().__init__(cityCode) - self.nodeId = nodeId - self.nodeNm = nodeNm - self.routeId = routeId - self.routeNo = routeNo - self.routeTp = routeTp + self.node = node + self.route = route self.arrprevstationcnt = arrprevstationcnt self.vehicleTp = vehicleTp self.arrtime = arrtime @@ -37,11 +35,8 @@ def to_dict(self): @classmethod def from_dict(cls, data: dict) -> "ArrivalInfo": return cls( - nodeId=data.get("nodeid"), - nodeNm=data.get("nodeid"), - routeTp=data.get("routetp"), - routeId = data.get("routeid"), - routeNo = data.get("routeno"), + node=Station.from_dict(data), + route=Route.from_dict(data["route"]), arrprevstationcnt = data.get("arrprevstationcnt"), vehicleTp = data.get("vehicletp"), arrtime = data.get("arrtime") diff --git a/tagoapi/models/Vehicle.py b/tagoapi/models/Vehicle.py index ddc4e88..9f987f6 100644 --- a/tagoapi/models/Vehicle.py +++ b/tagoapi/models/Vehicle.py @@ -1,5 +1,8 @@ from .Route import Route from .BaseModel import BaseModel +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .Route import Route class Vehicle(BaseModel): _lazy_fields = { @@ -10,8 +13,7 @@ class Vehicle(BaseModel): def __init__( self, cityCode: int, - routeId: str = None, - routeNo: str = None, + route: Route, gpsLati: float = None, gpsLong: float = None, arrtime: int = None, @@ -20,8 +22,7 @@ def __init__( vehicleNo: str = None ): super().__init__(cityCode) - self.routeId = routeId - self.routeNo = routeNo + self.route = route self.gpsLati = gpsLati self.gpsLong = gpsLong self.arrtime = arrtime @@ -38,9 +39,7 @@ def to_dict(self): @classmethod def from_dict(cls, data: dict) -> "Vehicle": return cls( - route=data.get("route"), - routeId=data.get("routeid"), - routeNo=data.get("routeno", data.get("routenm")), + route=Route.from_dict(data), gpsLati=data.get("gpslati"), gpsLong=data.get("gpslong"), arrtime=data.get("arrtime"), From afbaee52f97562bd3a7dd9c2d7869a4a8223c8ab Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:25:00 +0900 Subject: [PATCH 23/27] =?UTF-8?q?docs:=20README=20=EA=B0=9C=EC=B2=B4=20?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 106 +++++++++++++++++++++++++++++------------------------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index c223c9f..2ffa92b 100644 --- a/README.md +++ b/README.md @@ -76,63 +76,71 @@ print(stations) - `Route` : 버스 노선 정보 - `ArrivalInfo` : 버스 도착 정보 -#### 공통 메서드 + +--- + +### 도메인 객체 필드 목록 + +#### **BaseModel** +도메인 객체들의 상위 객체 입니다. + +다음과 같은 공통 메서드를 제공합니다. ~~~python obj.to_dict() # 객체 → dict 변환 BaseModel.from_dict(dict) # dict → 객체 변환 BaseModel.from_list(list) # dict 리스트 → 객체 리스트 변환 ~~~ ---- - -### 도메인 객체 필드 목록 - -#### **Station** -| 필드명 | 타입 | 설명 | -|--------|------|------| -| `nodeId` | `str` | 정류소 ID | -| `nodeNm` | `str` | 정류소명 | -| `nodeNo` | `int` | 정류소 번호 | -| `gpsLati` | `float` | 위도 (WGS84) | -| `gpsLong` | `float` | 경도 (WGS84) | -| `cityCode` | `int` | 도시코드 | -| `updowncd` | `int` | 상하행구분코드 (`0`: 상행, `1`: 하행) | -| `nodeord` | `int` | 정류소순번 | - -#### **Route** -| 필드명 | 타입 | 설명 | -|--------|------|------| -| `routeId` | `str` | 노선 ID | -| `routeNo` | `str` | 노선명 | -| `routeTp` | `int` | 노선유형 | -| `endNodeNm` | `str` | 종점 | -| `startNodeNm` | `str` | 기점 | -| `endvehicletime` | `int` | 막차시간 | -| `startvehicletime` | `int` | 첫차시간 | - -#### **ArrivalInfo** -| 필드명 | 타입 | 설명 | -|--------|------|------| -| `nodeId` | `str` | 정류소 ID | -| `nodeNm` | `str` | 정류소명 | -| `routeId` | `str` | 노선 ID | -| `routeNo` | `str` | 노선명 | -| `routeTp` | `int` | 노선유형 | -| `arrprevstationcnt` | `int` | 노선유형 | -| `vehicletp` | `str` | 차랑유형 | -| `arrtime` | `int` | 도착예상시간 | - -#### **Vehicle** -| 필드명 | 타입 | 설명 | -|--------|------|------| -| `routeId` | `str` | 노선 ID | -| `routeNo` | `str` | 노선명 | +### **BaseList** +도메인 객체들을 요소로 갖는 list 입니다. +기본적으로 Python의 기본 자료형인 list를 상속 받습니다. + +### **Station** +| 필드명 | 타입 | 설명 | +|------------|-------------------|------------------------| +| `nodeId` | `str` | 정류소 ID | +| `nodeNm` | `str` | 정류소명 | +| `nodeNo` | `int` | 정류소 번호 | +| `gpsLati` | `float` | 위도 (WGS84) | +| `gpsLong` | `float` | 경도 (WGS84) | +| `cityCode` | `int` | 도시코드 | +| `updowncd` | `int` | 상하행구분코드 (`0`: 상행, `1`: 하행) | +| `nodeord` | `int` | 정류소순번 | +| `routes` | `BaseList[Route]` | 정류소를 경유하는 노선 | + + +### **Route** +| 필드명 | 타입 | 설명 | +|--------------------|---------------------|--------------| +| `routeId` | `str` | 노선 ID | +| `routeNo` | `str` | 노선명 | +| `routeTp` | `int` | 노선유형 | +| `endNodeNm` | `str` | 종점 | +| `startNodeNm` | `str` | 기점 | +| `endvehicletime` | `int` | 막차시간 | +| `startvehicletime` | `int` | 첫차시간 | +| `stations` | `BaseList[Station]` | 노선이 경유하는 정류소 | + + +### **ArrivalInfo** +| 필드명 | 타입 | 설명 | +|---------------------|-----------|--------| +| `node` | `Station` | 정류소 정보 | +| `route` | `Route` | 노선 정보 | +| `arrprevstationcnt` | `int` | 노선유형 | +| `vehicletp` | `str` | 차랑유형 | +| `arrtime` | `int` | 도착예상시간 | + +### **Vehicle** +| 필드명 | 타입 | 설명 | +|--------|---------|------------| +| `route` | `Route` | 노선 정보 | | `gpsLati` | `float` | 위도 (WGS84) | | `gpsLong` | `float` | 경도 (WGS84) | -| `arrtime` | `int` | 도착예상시간 | -| `arrprevstationcnt` | `int` | 노선유형 | -| `vehicleTp` | `str` | 차랑유형 | -| `vehicleNo` | `str` | 차랑번호 | +| `arrtime` | `int` | 도착예상시간 | +| `arrprevstationcnt` | `int` | 노선유형 | +| `vehicleTp` | `str` | 차랑유형 | +| `vehicleNo` | `str` | 차랑번호 | From a01b9715bcb3900b5e47ed2d95a68db542116a58 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:03:43 +0900 Subject: [PATCH 24/27] refactor: refactor utils --- tagoapi/__init__.py | 10 +-- tagoapi/client.py | 2 +- tagoapi/models/BaseModel.py | 7 ++ tagoapi/models/__init__.py | 3 +- tagoapi/utils/__init__.py | 16 ++--- tagoapi/utils/cache/__init__.py | 4 ++ tagoapi/utils/{ => cache}/cache.py | 8 +++ tagoapi/utils/decorator/__init__.py | 3 + .../convert_model.py} | 17 +++-- tagoapi/utils/params.py | 67 +++++++++++++++++-- tagoapi/utils/parser.py | 58 ---------------- 11 files changed, 105 insertions(+), 90 deletions(-) create mode 100644 tagoapi/utils/cache/__init__.py rename tagoapi/utils/{ => cache}/cache.py (87%) create mode 100644 tagoapi/utils/decorator/__init__.py rename tagoapi/utils/{cache_util.py => decorator/convert_model.py} (68%) delete mode 100644 tagoapi/utils/parser.py diff --git a/tagoapi/__init__.py b/tagoapi/__init__.py index 68d9c50..4ba4321 100644 --- a/tagoapi/__init__.py +++ b/tagoapi/__init__.py @@ -3,16 +3,12 @@ from .auth import TAGOAuth -from .models import Route -from .models import Vehicle -from .models import Station +from .models import * # from .utils import get_city_code -from .utils.cache_util import convert_model -from .utils.parser import KeyExtract -from .utils.cache_util import cache + from .utils.get_station import get_station -__all__ = [ 'TAGOClient', 'TAGOAuth', 'convert_model', 'Route', 'Vehicle', 'Station', 'KeyExtract', 'cache', 'get_station' ] \ No newline at end of file +__all__ = [ 'TAGOClient', 'TAGOAuth', 'BaseModel', 'BaseList', 'Route', 'Vehicle', 'Station', 'get_station' ] \ No newline at end of file diff --git a/tagoapi/client.py b/tagoapi/client.py index 7bb35cd..5aba3e2 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -3,7 +3,7 @@ from .models import * from .auth import TAGOAuth -from typing import Union, Optional, overload +from typing import Optional, overload diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index 78120d0..b5112db 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -1,10 +1,13 @@ from typing import TYPE_CHECKING from tagoapi.models.BaseList import BaseList +from tagoapi.utils import KeyExtract + if TYPE_CHECKING: from tagoapi import TAGOClient class BaseModel: cache_key = "BaseModel:" + _key = KeyExtract(cache_key) _lazy_fields: dict = {} def __init__(self, cityCode: int): @@ -66,3 +69,7 @@ def __getattribute__(self, item): raise AttributeError(f"{self.__class__.__name__} object has no attribute {item}") + @property + def key(self) -> KeyExtract: + return self._key + diff --git a/tagoapi/models/__init__.py b/tagoapi/models/__init__.py index bc86fe0..afea18b 100644 --- a/tagoapi/models/__init__.py +++ b/tagoapi/models/__init__.py @@ -3,5 +3,6 @@ from .Station import Station from .ArrivalInfo import ArrivalInfo from .BaseModel import BaseModel +from .BaseList import BaseList -__all__ = ['Route', 'Vehicle', 'Station', 'ArrivalInfo', 'BaseModel'] \ No newline at end of file +__all__ = ['Route', 'Vehicle', 'Station', 'ArrivalInfo', 'BaseModel', 'BaseList'] \ No newline at end of file diff --git a/tagoapi/utils/__init__.py b/tagoapi/utils/__init__.py index a21262f..7d8c70b 100644 --- a/tagoapi/utils/__init__.py +++ b/tagoapi/utils/__init__.py @@ -1,23 +1,19 @@ -from .cache import Cache -from .cache_util import convert_model -from .cache_util import cache -from .parser import parse_metadata from .convertor import convert -from .parser import KeyExtract + +from .params import KeyExtract from .params import build_params + from .get import http_get + from .get_station import get_station __all__ = [ - "Cache", - "KeyExtract", - "convert_model", - "parse_metadata", "convert", "build_params", "cache", "http_get", - "get_station" + "get_station", + "KeyExtract" ] \ No newline at end of file diff --git a/tagoapi/utils/cache/__init__.py b/tagoapi/utils/cache/__init__.py new file mode 100644 index 0000000..8bfb13a --- /dev/null +++ b/tagoapi/utils/cache/__init__.py @@ -0,0 +1,4 @@ +from .cache import cache +from .cache import _generate_cache_key + +__all__ = ["cache", "_generate_cache_key"] \ No newline at end of file diff --git a/tagoapi/utils/cache.py b/tagoapi/utils/cache/cache.py similarity index 87% rename from tagoapi/utils/cache.py rename to tagoapi/utils/cache/cache.py index 427c187..8570c16 100644 --- a/tagoapi/utils/cache.py +++ b/tagoapi/utils/cache/cache.py @@ -65,3 +65,11 @@ def _dump(self): @property def current_cache(self): return self._cache + +cache = Cache() + + +def _generate_cache_key(*args, _fname: str, **kwargs) -> str: + return _fname + ":" + "&".join([str(a) for a in args]) + "&".join( + f"{key}={value}" for key, value in kwargs.items() + ) ## str로 나타낼 수 없으면 다르게 표시하도록 \ No newline at end of file diff --git a/tagoapi/utils/decorator/__init__.py b/tagoapi/utils/decorator/__init__.py new file mode 100644 index 0000000..7e2fa11 --- /dev/null +++ b/tagoapi/utils/decorator/__init__.py @@ -0,0 +1,3 @@ +from convert_model import convert_model + +__all__=["convert_model"] \ No newline at end of file diff --git a/tagoapi/utils/cache_util.py b/tagoapi/utils/decorator/convert_model.py similarity index 68% rename from tagoapi/utils/cache_util.py rename to tagoapi/utils/decorator/convert_model.py index ade0b2e..0fd2127 100644 --- a/tagoapi/utils/cache_util.py +++ b/tagoapi/utils/decorator/convert_model.py @@ -1,25 +1,28 @@ from functools import wraps from typing import TYPE_CHECKING -from .cache import Cache -from ..models.BaseList import BaseList + +from tagoapi.utils.cache import cache, _generate_cache_key +from tagoapi.models.BaseList import BaseList if TYPE_CHECKING: from tagoapi.models import BaseModel -cache = Cache() + # method에서만 사용할 함수 -def convert_model(ttl: int = 86400, model: type["BaseModel"] = None, is_cached: bool = True, is_list: bool = True): # 데코레이터가 사용할 매개변수 +def convert_model(ttl: int = 86400, model: type["BaseModel"] = None, use_model: bool = False, is_cached: bool = True, is_list: bool = True): # 데코레이터가 사용할 매개변수 def decorator(fn): # 호출할 함수를 매개변수로 받음 def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아서 이를 실행 - key = _make_cache_key(*args, _fname=fn.__name__, **kwargs) if is_cached else None + key = _generate_cache_key(*args, _fname=fn.__name__, **kwargs) if is_cached else None cached = cache.get(key) if key else None if cached is None: raw = fn(self, *args, **kwargs) if key: + if use_model: + cache.save(model.key.generate_key(raw)) cache.save(key, raw, ttl) else: raw = cached @@ -39,7 +42,3 @@ def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아 return inner return decorator -def _make_cache_key(*args, _fname: str, **kwargs) -> str: - return _fname + ":" + "&".join([str(a) for a in args]) + "&".join( - f"{key}={value}" for key, value in kwargs.items() - ) ## str로 나타낼 수 없으면 다르게 표시하도록 \ No newline at end of file diff --git a/tagoapi/utils/params.py b/tagoapi/utils/params.py index b7d9afd..5d09fb5 100644 --- a/tagoapi/utils/params.py +++ b/tagoapi/utils/params.py @@ -1,19 +1,78 @@ from typing import TYPE_CHECKING +from typing import TypeVar + +U = TypeVar('U', dict[str], list) # list, dict으로 반환할 때 + if TYPE_CHECKING: from tagoapi import TAGOAuth def build_params( - auth: "TAGOAuth", - numOfRows: int = 300, + auth: "TAGOAuth", + numOfRows: int = 300, pageNo: int = 1, **kwargs: dict ) -> dict: - + return { "serviceKey": auth.serviceKey, "numOfRows": numOfRows, "pageNo": pageNo, "_type": "json", **{key: value for key, value in kwargs.items() if value} - } \ No newline at end of file + } + +def parse_metadata(res: dict) -> U | None: + striped = res.get("response", {}).get("body", {}).get("items", {}) + if isinstance(striped, dict): + return striped.get("item", None) + + return None + + +def _check_bracket(data: str) -> list[str]: + args = [] + current_match = None + fa = '' + for w in data: + if current_match: + fa += w + + if w == '<': + current_match = True + elif w == '>': + if not current_match: + return None # 에러를 나타내든 뭐를 하든 + current_match = False + args.append(fa[:-1].strip()); + fa = '' + + return args + + +class KeyExtract: ## 이를 BaseModel에 바로/ + def __init__(self, raw_key): + self.raw_key = raw_key + + self._args = _check_bracket(self.raw_key) + + @property + def key_args(self): + return self._args + + def generate_key(self, data: dict) -> str: + generated_key = self.raw_key + # if len(kwargs) > len(self._args): + # raise TypeError(f"generate_key() takes {len(self._args)} positional argument but {len(kwargs)} were given") + + # TODO: 이거 self._args말고 kwargs.keys해서 arg랑 대응 시켜서 없으면 raise 이런식으로 작성해도 좋을 듯 + for arg in self._args: + k = data.get(arg.lower(), None) + if not k: + raise TypeError() + generated_key = generated_key.replace(f"<{arg}>", k) + + return generated_key + + def __call__(self): + pass diff --git a/tagoapi/utils/parser.py b/tagoapi/utils/parser.py deleted file mode 100644 index d0bc0ad..0000000 --- a/tagoapi/utils/parser.py +++ /dev/null @@ -1,58 +0,0 @@ -from tagoapi.models import BaseModel -from typing import Union, Callable, TypeVar, Any -import os - -U = TypeVar('U', dict[str], list) # list, dict으로 반환할 때 - -def parse_metadata(res: dict) -> U | None: - striped = res.get("response", {}).get("body", {}).get("items", {}) - if isinstance(striped, dict): - return striped.get("item", None) - - return None - -class KeyExtract: - def __init__(self, model: BaseModel): - self.model = model - self.raw_key = model.cache_key - - self._args = self._check_bracket(self.raw_key) - - @property - def key_args(self): - return self._args - - def generate_key(self, data: dict) -> str: - generated_key = self.raw_key - # if len(kwargs) > len(self._args): - # raise TypeError(f"generate_key() takes {len(self._args)} positional argument but {len(kwargs)} were given") - - #TODO: 이거 self._args말고 kwargs.keys해서 arg랑 대응 시켜서 없으면 raise 이런식으로 작성해도 좋을 듯 - for arg in self._args: - k = data.get(arg.lower(), None) - if not k: - raise TypeError() - generated_key = generated_key.replace(f"<{arg}>", k) - - return generated_key - - def __call__(self): - pass - - def _check_bracket(self, data: str) -> list[str]: - args = [] - current_match = None - fa = '' - for w in data: - if current_match: - fa += w - - if w == '<': - current_match = True - elif w == '>': - if not current_match: - return None # 에러를 나타내든 뭐를 하든 - current_match = False - args.append(fa[:-1].strip()); fa = '' - - return args From 6f5ec28ff09e28b66b104c78f1d29a7018682281 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:56:07 +0900 Subject: [PATCH 25/27] =?UTF-8?q?test:=20=EC=A1=B0=ED=9A=8C=20=ED=9B=84=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=EC=A0=80=EC=9E=A5=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/utils/get_station.py | 90 ++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/tagoapi/utils/get_station.py b/tagoapi/utils/get_station.py index 17863f4..1e3ed32 100644 --- a/tagoapi/utils/get_station.py +++ b/tagoapi/utils/get_station.py @@ -1,45 +1,45 @@ -import pandas as pd, os -from tagoapi.models import Station -from tagoapi.exceptions import * -from .cache import Cache -# -# def csv_to_dict(csvfile, encoding): -# loaded_csv = pd.read_csv(csvfile, encoding=encoding) -# data = loaded_csv.to_dict(orient='records') -# return data -# MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) -# PROJECT_ROOT = os.path.abspath(os.path.join(MODULE_DIR, "..")) -# CACHE_DIR = os.path.join(PROJECT_ROOT, "caches") -# STATION_CACHE_PATH = os.path.join(CACHE_DIR, "station.pkl") -# -# cache = Cache(STATION_CACHE_PATH) -# station_list = cache.get("stations_2025_06_15.csv") -# if not station_list: -# station_list = csv_to_dict("tagoapi/csv/stations_2025_06_15.csv", 'cp949') -# cache.save("stations_2025_06_15.csv", station_list) -# -def get_station(keyword) -> list[Station]: pass -# # print(cache.current_cache) -# result = [] -# try: -# for station in station_list: -# if keyword in station["정류장명"]: -# result.append( -# Station.from_dict({ -# "nodeid": station["정류장번호"], -# "nodenm": station["정류장명"], -# "nodeno": station["모바일단축번호"], -# "gpslati": station["위도"], -# "gpslong": station["경도"], -# "citycode": station["도시코드"] -# }) -# ) -# -# -# return result -# except TypeError as e: -# raise CacheNotFoundError("정류소 캐시파일을 불러오지 못했습니다.") -# -# -# -# +# import pandas as pd, os +# from tagoapi.models import Station +# from tagoapi.exceptions import * +# from .cache import Cache +# # +# # def csv_to_dict(csvfile, encoding): +# # loaded_csv = pd.read_csv(csvfile, encoding=encoding) +# # data = loaded_csv.to_dict(orient='records') +# # return data +# # MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) +# # PROJECT_ROOT = os.path.abspath(os.path.join(MODULE_DIR, "..")) +# # CACHE_DIR = os.path.join(PROJECT_ROOT, "caches") +# # STATION_CACHE_PATH = os.path.join(CACHE_DIR, "station.pkl") +# # +# # cache = Cache(STATION_CACHE_PATH) +# # station_list = cache.get("stations_2025_06_15.csv") +# # if not station_list: +# # station_list = csv_to_dict("tagoapi/csv/stations_2025_06_15.csv", 'cp949') +# # cache.save("stations_2025_06_15.csv", station_list) +# # +# def get_station(keyword) -> list[Station]: pass +# # # print(cache.current_cache) +# # result = [] +# # try: +# # for station in station_list: +# # if keyword in station["정류장명"]: +# # result.append( +# # Station.from_dict({ +# # "nodeid": station["정류장번호"], +# # "nodenm": station["정류장명"], +# # "nodeno": station["모바일단축번호"], +# # "gpslati": station["위도"], +# # "gpslong": station["경도"], +# # "citycode": station["도시코드"] +# # }) +# # ) +# # +# # +# # return result +# # except TypeError as e: +# # raise CacheNotFoundError("정류소 캐시파일을 불러오지 못했습니다.") +# # +# # +# # +# # From fd5959a9bd21edb40755ab2a0e2d2f5b19f90571 Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:37:57 +0900 Subject: [PATCH 26/27] =?UTF-8?q?chore:=20cache=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/__init__.py | 4 ++-- tagoapi/caches/cache.pkl | Bin 529 -> 3066 bytes tagoapi/client.py | 4 +++- tagoapi/models/ArrivalInfo.py | 4 ++-- tagoapi/models/BaseModel.py | 4 ++-- tagoapi/utils/__init__.py | 7 +++---- tagoapi/utils/decorator/__init__.py | 2 +- tagoapi/utils/decorator/convert_model.py | 16 ++++++++++------ tests/test_client.py | 3 ++- tests/test_get.py | 22 ++++++++++++++++++++++ tests/test_object_station.py | 13 +++++++++++++ 11 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 tests/test_get.py diff --git a/tagoapi/__init__.py b/tagoapi/__init__.py index 4ba4321..142feaa 100644 --- a/tagoapi/__init__.py +++ b/tagoapi/__init__.py @@ -7,8 +7,8 @@ # from .utils import get_city_code -from .utils.get_station import get_station -__all__ = [ 'TAGOClient', 'TAGOAuth', 'BaseModel', 'BaseList', 'Route', 'Vehicle', 'Station', 'get_station' ] \ No newline at end of file + +__all__ = [ 'TAGOClient', 'TAGOAuth', 'BaseModel', 'BaseList', 'Route', 'Vehicle', 'Station' ] \ No newline at end of file diff --git a/tagoapi/caches/cache.pkl b/tagoapi/caches/cache.pkl index 68421c3325832e2d7a562e5726c7db68dff36a70..efe73f13861d1b227dd304adf60769dec56d966f 100644 GIT binary patch literal 3066 zcmZo*nfjia0Ss!VX!M8%mn4>C=I2?txI38}7#JEDSeh6ZOaV!;mL=wtrh=L5=>^3( zKy_2x9UR}?W4O3oyaz0lpO+34N)R@VRhj47!QFo;B6?vx&$H%)t9 z_fCDab;8?OJyW3K`BQwe{Yrb7OGCvf^q6JF;^?>N7Zd_#y6i)3Wh(_s%y{ z=S|7r_HFYk&ENsspTP^Y^vyJMOU(=ojE#)YEq%4>(R9mPu%)6~71-~(z%7NV2U^Mn zvGjHSOhb@;hcNB?=!VrkV+{Lt&p7_531Z*!!jlC*Q0)V$N3qWcWZy|l`@Y;M?NLon zEr~CNCW7S5l1gV_B-$F8s(~WYFW2_X^p@B2)*>f}*eM#IGkwG8zP4N%fag{7T9MvSy$=AG-M^bDsrjc|LDPJYDa_JSi+*`Pgtu4?9>Q511}e7@?e!f+;=Bz!U{@B``Tr list | dict: response = parse_metadata(self._get(endpoint, params)) - return {**response, **kwargs} + return {"result":response, "cityCode": citycode} def _get(self, endpoint: str, params: dict) -> any: response = http_get(f"{self.BASE_URL}/{endpoint}", params=params) diff --git a/tagoapi/models/ArrivalInfo.py b/tagoapi/models/ArrivalInfo.py index 8ac5c66..2cea527 100644 --- a/tagoapi/models/ArrivalInfo.py +++ b/tagoapi/models/ArrivalInfo.py @@ -12,8 +12,8 @@ class ArrivalInfo(BaseModel): } def __init__(self, - node: Station, - route: Route, + node: "Station", + route: "Route", cityCode, arrprevstationcnt: int = None, vehicleTp: str = None, diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index b5112db..db8a48f 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -24,8 +24,8 @@ def set_client(self, client: "TAGOClient"): def from_dict(cls, data: dict) -> "BaseModel": ... @classmethod - def from_list(cls, data: list) -> BaseList: - return BaseList([cls.from_dict(d) for d in data]) + def from_list(cls, data: list, cityCode: str) -> BaseList: + return BaseList([cls.from_dict({**d, "cityCode": cityCode}) for d in data]) # model = BaseList() # for element in data: # model.append(cls.from_dict(element)) diff --git a/tagoapi/utils/__init__.py b/tagoapi/utils/__init__.py index 7d8c70b..c159a86 100644 --- a/tagoapi/utils/__init__.py +++ b/tagoapi/utils/__init__.py @@ -1,19 +1,18 @@ from .convertor import convert -from .params import KeyExtract -from .params import build_params +from .params import * from .get import http_get -from .get_station import get_station +# from .get_station import get_station __all__ = [ + "parse_metadata", "convert", "build_params", "cache", "http_get", - "get_station", "KeyExtract" ] \ No newline at end of file diff --git a/tagoapi/utils/decorator/__init__.py b/tagoapi/utils/decorator/__init__.py index 7e2fa11..3e1bac8 100644 --- a/tagoapi/utils/decorator/__init__.py +++ b/tagoapi/utils/decorator/__init__.py @@ -1,3 +1,3 @@ -from convert_model import convert_model +from .convert_model import convert_model __all__=["convert_model"] \ No newline at end of file diff --git a/tagoapi/utils/decorator/convert_model.py b/tagoapi/utils/decorator/convert_model.py index 0fd2127..71d8ede 100644 --- a/tagoapi/utils/decorator/convert_model.py +++ b/tagoapi/utils/decorator/convert_model.py @@ -11,13 +11,18 @@ # method에서만 사용할 함수 -def convert_model(ttl: int = 86400, model: type["BaseModel"] = None, use_model: bool = False, is_cached: bool = True, is_list: bool = True): # 데코레이터가 사용할 매개변수 +def convert_model( + ttl: int = 86400, + model: type["BaseModel"] = None, + use_model: bool = False, + is_cached: bool = True, + is_list: bool = True +): # 데코레이터가 사용할 매개변수 def decorator(fn): # 호출할 함수를 매개변수로 받음 def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아서 이를 실행 key = _generate_cache_key(*args, _fname=fn.__name__, **kwargs) if is_cached else None cached = cache.get(key) if key else None - if cached is None: raw = fn(self, *args, **kwargs) if key: @@ -26,13 +31,12 @@ def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아 cache.save(key, raw, ttl) else: raw = cached - if model: ## convert list - if isinstance(raw, list): - res = model.from_list(raw) + if isinstance(raw.get("result"), list): + res = model.from_list(raw.get("result"), raw.get("cityCode")) else: - res = model.from_dict(raw) + res = model.from_dict({**(raw.get("result")), **raw.get("cityCode")}) # print(is_list and not isinstance(res, BaseList)) res.set_client(self) return BaseList([res]) if is_list and not isinstance(res, BaseList) else res diff --git a/tests/test_client.py b/tests/test_client.py index f6c2a53..17be88b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import patch -from tagoapi import TAGOClient, TAGOAuth, cache +from tagoapi import TAGOClient, TAGOAuth +from tagoapi.utils.cache import cache class TestTagoClient(unittest.TestCase): def setUp(self): diff --git a/tests/test_get.py b/tests/test_get.py new file mode 100644 index 0000000..57e8b66 --- /dev/null +++ b/tests/test_get.py @@ -0,0 +1,22 @@ +from tagoapi import TAGOAuth, TAGOClient +from tagoapi.utils.cache import cache +from pprint import pprint +import time + +import os +from dotenv import load_dotenv +env_path = os.path.join(os.path.dirname(__file__), '..', '.env') +load_dotenv(dotenv_path=env_path) + +api_key = os.environ.get("TAGO_API_KEY") +client = TAGOClient(TAGOAuth(api_key)) + +# print(cache.current_cache) +# print(client.get_route_by_no(routeNo="북구", cityCode=22)) +print(cache.current_cache) + +route = client.get_route_by_no(routeNo="북구", cityCode=22) + +# pprint(cache.current_cache) +print(route[0].stations) + diff --git a/tests/test_object_station.py b/tests/test_object_station.py index aff8e59..f895eed 100644 --- a/tests/test_object_station.py +++ b/tests/test_object_station.py @@ -1,3 +1,4 @@ +import copy import unittest from unittest.mock import MagicMock @@ -24,6 +25,18 @@ def test_RuntimeError(self): ## client 주입 테스트 with self.assertRaises(RuntimeError): print("endtime:", self.station.routes) + def test_lazy_loading_station(self): + print("\n====== test lazy_loading_station ( attribute not in class ======") + + station = copy.deepcopy(self.station) + + mock_client = MagicMock() + mock_client._get_routes_by_station.return_value = BaseList([Route("564", routeNo="북구2", cityCode=22), Route("564", routeNo="북구2", cityCode=22), Route("564", routeNo="북구2", cityCode=22)]) + station.set_client(mock_client) + + print(station.routes) + + # def test_lazy_load_not_in_class(self): ## lazy_load ( attribute not in class ) # print("\n====== test lazy_load ( attribute not in class ) ======") # From 72faddcb3a17e98d5812f292f65d3cfad87b9bcf Mon Sep 17 00:00:00 2001 From: hyuntroll <83080414+hyuntroll@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:20:09 +0900 Subject: [PATCH 27/27] =?UTF-8?q?chore:=20model=20convert=20=EC=8B=9C=20ci?= =?UTF-8?q?tycode=EA=B0=80=20=EB=88=84=EB=9D=BD=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tagoapi/models/BaseModel.py | 7 +------ tagoapi/utils/decorator/convert_model.py | 6 +++--- tests/test_get.py | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tagoapi/models/BaseModel.py b/tagoapi/models/BaseModel.py index db8a48f..62b4ef9 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -24,13 +24,8 @@ def set_client(self, client: "TAGOClient"): def from_dict(cls, data: dict) -> "BaseModel": ... @classmethod - def from_list(cls, data: list, cityCode: str) -> BaseList: + def from_list(cls, data: list, cityCode: int) -> BaseList: return BaseList([cls.from_dict({**d, "cityCode": cityCode}) for d in data]) - # model = BaseList() - # for element in data: - # model.append(cls.from_dict(element)) - # - # return model def __getattr__(self, item): diff --git a/tagoapi/utils/decorator/convert_model.py b/tagoapi/utils/decorator/convert_model.py index 71d8ede..2815cf9 100644 --- a/tagoapi/utils/decorator/convert_model.py +++ b/tagoapi/utils/decorator/convert_model.py @@ -34,10 +34,10 @@ def inner(self, *args, **kwargs): # 호출할 함수의 매개변수를 받아 if model: ## convert list if isinstance(raw.get("result"), list): - res = model.from_list(raw.get("result"), raw.get("cityCode")) + res = model.from_list(raw.get("result"), raw.get("citycode")) else: - res = model.from_dict({**(raw.get("result")), **raw.get("cityCode")}) - # print(is_list and not isinstance(res, BaseList)) + res = model.from_dict({**(raw.get("result")), "citycode": raw.get("cityCode")}) + res.set_client(self) return BaseList([res]) if is_list and not isinstance(res, BaseList) else res diff --git a/tests/test_get.py b/tests/test_get.py index 57e8b66..f4a1b6f 100644 --- a/tests/test_get.py +++ b/tests/test_get.py @@ -15,7 +15,7 @@ # print(client.get_route_by_no(routeNo="북구", cityCode=22)) print(cache.current_cache) -route = client.get_route_by_no(routeNo="북구", cityCode=22) +route = client.get_route_by_no(routeNo="북구1", cityCode=22) # pprint(cache.current_cache) print(route[0].stations)