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/.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/README.md b/README.md index 9d56181..2ffa92b 100644 --- a/README.md +++ b/README.md @@ -76,63 +76,71 @@ print(stations) - `Route` : 버스 노선 정보 - `ArrivalInfo` : 버스 도착 정보 -#### 공통 메서드 -~~~python -obj.to_dict() # 객체 → dict 변환 -ClassName.from_dict(dict) # dict → 객체 변환 -ClassName.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` | 노선명 | +#### **BaseModel** +도메인 객체들의 상위 객체 입니다. + +다음과 같은 공통 메서드를 제공합니다. +~~~python +obj.to_dict() # 객체 → dict 변환 +BaseModel.from_dict(dict) # dict → 객체 변환 +BaseModel.from_list(list) # dict 리스트 → 객체 리스트 변환 +~~~ + +### **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` | 차랑번호 | @@ -153,4 +161,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)에 등록해주세요. diff --git a/tagoapi/__init__.py b/tagoapi/__init__.py index de46ab4..142feaa 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 from_cache_or_fetch -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', 'BaseModel', 'BaseList', 'Route', 'Vehicle', 'Station' ] \ No newline at end of file diff --git a/tagoapi/caches/cache.pkl b/tagoapi/caches/cache.pkl index 48379cc..efe73f1 100644 Binary files a/tagoapi/caches/cache.pkl and b/tagoapi/caches/cache.pkl differ diff --git a/tagoapi/caches/station.pkl b/tagoapi/caches/station.pkl index f30f43d..a47bce1 100644 Binary files a/tagoapi/caches/station.pkl and b/tagoapi/caches/station.pkl differ diff --git a/tagoapi/client.py b/tagoapi/client.py index f8e5ff2..d37ce5d 100644 --- a/tagoapi/client.py +++ b/tagoapi/client.py @@ -1,9 +1,10 @@ from .exceptions import * +from .utils.decorator import * from .utils import * from .models import * from .auth import TAGOAuth -from typing import Union, Optional, overload +from typing import Optional, overload @@ -27,7 +28,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) + @convert_model(604800, Route) def get_route_by_no( self, cityCode: int, @@ -36,9 +37,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, citycode=cityCode) - @from_cache_or_fetch(604800) + @convert_model(604800, Route, is_list=False) def get_route_by_id( self, cityCode: int, @@ -47,9 +48,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, citycode=cityCode) - @from_cache_or_fetch(604800) + @convert_model(604800, Route) def get_route_by_station( self, cityCode: int, @@ -58,10 +59,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, citycode=cityCode) - @from_cache_or_fetch(604800) + @convert_model(604800, Station) def get_station_by_route( self, cityCode: int, @@ -70,9 +71,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, citycode=cityCode) - @from_cache_or_fetch(86400) + @convert_model(86400, Station) def get_station( self, cityCode: int, @@ -85,20 +86,20 @@ 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, citycode=cityCode) - @from_cache_or_fetch(86400) + @convert_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) + @convert_model(model=ArrivalInfo, is_cached=False) def get_arrival_by_station( self, cityCode: int, @@ -107,8 +108,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, citycode=cityCode) + + @convert_model(model=ArrivalInfo, is_cached=False) def get_route_arrival_by_station( self, cityCode: int, @@ -118,9 +120,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, citycode=cityCode) - + @convert_model(model=Vehicle, is_cached=False) def get_route_pos( self, cityCode: int, @@ -129,8 +131,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, citycode=cityCode) + @convert_model(model=Vehicle, is_cached=False) def get_route_pos_near_station( self, cityCode: int, @@ -140,70 +143,65 @@ 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, 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_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) + + 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 ################ 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, + citycode: int, + **kwargs + ) -> list | dict: response = parse_metadata(self._get(endpoint, params)) - if not response: - return None - - ## Convert to List - if isinstance(response, list): - - 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 + return {"result":response, "cityCode": citycode} - 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': 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: - raise RuntimeError(f"실행중 오류가 발생했습니다. 에러코드: {error_code}") - - return response \ No newline at end of file + else: + raise RuntimeError(f"실행중 오류가 발생했습니다. 에러코드: {error_code}") \ No newline at end of file diff --git a/tagoapi/models/ArrivalInfo.py b/tagoapi/models/ArrivalInfo.py index 2b877ae..2cea527 100644 --- a/tagoapi/models/ArrivalInfo.py +++ b/tagoapi/models/ArrivalInfo.py @@ -1,22 +1,27 @@ from .BaseModel import BaseModel +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .Station import Station + from .Route import Route class ArrivalInfo(BaseModel): + _lazy_fields = { + "station": "_get_station_by_arrival_info", + "routes": "_get_route_by_arrival_info" + } + def __init__(self, - nodeId, - nodeNm, - routeId: str, - routeNo: str, - routeTp, + node: "Station", + route: "Route", + cityCode, arrprevstationcnt: int = None, vehicleTp: str = None, arrtime: int = None ): - self.nodeId = nodeId - self.nodeNm = nodeNm - self.routeId = routeId - self.routeNo = routeNo - self.routeTp = routeTp + super().__init__(cityCode) + self.node = node + self.route = route self.arrprevstationcnt = arrprevstationcnt self.vehicleTp = vehicleTp self.arrtime = arrtime @@ -30,16 +35,13 @@ 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") ) - @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/BaseList.py b/tagoapi/models/BaseList.py new file mode 100644 index 0000000..40dd8dc --- /dev/null +++ b/tagoapi/models/BaseList.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from tagoapi.models.BaseModel import BaseModel + from tagoapi import TAGOClient + +class BaseList(list): + + def as_list(self) -> list["BaseModel"]: + return list(self) + + def set_client(self, client: "TAGOClient") -> "BaseList": + for element in self: + 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 92a3c47..62b4ef9 100644 --- a/tagoapi/models/BaseModel.py +++ b/tagoapi/models/BaseModel.py @@ -1,22 +1,70 @@ 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:" - def __init__(self, client: TAGOClient): - self._client = client - - def to_dict(self) -> dict: ... + _key = KeyExtract(cache_key) + _lazy_fields: dict = {} - def to_dict(self): + def __init__(self, cityCode: int): + self._client = None + self.cityCode = cityCode + + def to_dict(self) -> dict: 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, cityCode: int) -> BaseList: + return BaseList([cls.from_dict({**d, "cityCode": cityCode}) for d in data]) + + 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: + 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}") + + @property + def key(self) -> KeyExtract: + return self._key diff --git a/tagoapi/models/Route.py b/tagoapi/models/Route.py index c25b5d2..dcfb31f 100644 --- a/tagoapi/models/Route.py +++ b/tagoapi/models/Route.py @@ -8,20 +8,30 @@ # 버스 노선 자체에 관한 정보 class Route(BaseModel): cache_key = "Route:" + _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, startNodeNm: str = None, endvehicletime: int = None, startvehicletime: int = None, - client = None + intervalsattime: int = None, + intervalsuntime: int = None #TODO: 정류장 리스트도 넣으면 좋을 듯 합니당 ): - super().__init__(client) + super().__init__(cityCode) self.routeId = routeId self.routeNo = routeNo @@ -30,6 +40,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})" @@ -41,14 +53,17 @@ 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"), 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 - 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/Station.py b/tagoapi/models/Station.py index 1e5c335..8107551 100644 --- a/tagoapi/models/Station.py +++ b/tagoapi/models/Station.py @@ -1,11 +1,13 @@ from typing import TYPE_CHECKING from .BaseModel import BaseModel -if TYPE_CHECKING: - from .Route import Route class Station(BaseModel): # cache_key = "Station:" cache_key = "Station:" + _lazy_fields = { + "routes": "_get_routes_by_station", + "nodeNo": "_get_station", # csv에서 nodeId로 찾을 수 있도록 수정 + } def __init__( self, @@ -16,9 +18,9 @@ def __init__( gpsLong: float = None, cityCode: int = None, updowncd: int = None, - nodeord: int = None - # *routeList: list['Route'] + nodeord: int = None, ): + super().__init__(cityCode) self.nodeId = nodeId self.nodeNm = nodeNm self.nodeNo = nodeNo @@ -45,10 +47,10 @@ 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"), ) - @classmethod - def from_list(cls, data: list[dict]) -> list["Station"]: - return [cls.from_dict(station) 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 diff --git a/tagoapi/models/Vehicle.py b/tagoapi/models/Vehicle.py index 185a7ef..9f987f6 100644 --- a/tagoapi/models/Vehicle.py +++ b/tagoapi/models/Vehicle.py @@ -1,12 +1,19 @@ 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 = { + "route": '_get_route_by_vehicle', + "station": '_get_station_by_vehicle' + } + def __init__( self, - route: Route = None, - routeId: str = None, - routeNo: str = None, + cityCode: int, + route: Route, gpsLati: float = None, gpsLong: float = None, arrtime: int = None, @@ -14,9 +21,8 @@ def __init__( vehicleTp: str = None, vehicleNo: str = None ): + super().__init__(cityCode) self.route = route - self.routeId = routeId - self.routeNo = routeNo self.gpsLati = gpsLati self.gpsLong = gpsLong self.arrtime = arrtime @@ -33,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"), @@ -44,6 +48,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/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 ceaca27..c159a86 100644 --- a/tagoapi/utils/__init__.py +++ b/tagoapi/utils/__init__.py @@ -1,23 +1,18 @@ -from .cache import Cache -from .cache_util import from_cache_or_fetch -from .cache_util import cache -from .parser import parse_metadata from .convertor import convert -from .parser import KeyExtract -from .params import build_params -from .http import http_get -from .get_station import get_station + +from .params import * + +from .get import http_get + +# from .get_station import get_station __all__ = [ - "Cache", - "KeyExtract", - "from_cache_or_fetch", "parse_metadata", "convert", "build_params", "cache", "http_get", - "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 58% rename from tagoapi/utils/cache.py rename to tagoapi/utils/cache/cache.py index e6fdb0d..8570c16 100644 --- a/tagoapi/utils/cache.py +++ b/tagoapi/utils/cache/cache.py @@ -6,7 +6,13 @@ CACHE_DIR = os.path.join(PROJECT_ROOT, "caches") DEFAULT_CACHE_PATH = os.path.join(CACHE_DIR, "cache.pkl") + 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) @@ -19,21 +25,26 @@ 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 {} 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 @@ -44,6 +55,21 @@ 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 + +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/cache_util.py b/tagoapi/utils/cache_util.py deleted file mode 100644 index 507272e..0000000 --- a/tagoapi/utils/cache_util.py +++ /dev/null @@ -1,25 +0,0 @@ -from .cache import Cache - - -cache = Cache() - -# method에서만 사용할 함수 -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 - - return wrapper - return real_deco - -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/convertor.py b/tagoapi/utils/convertor.py index 8ca7411..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/decorator/__init__.py b/tagoapi/utils/decorator/__init__.py new file mode 100644 index 0000000..3e1bac8 --- /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/decorator/convert_model.py b/tagoapi/utils/decorator/convert_model.py new file mode 100644 index 0000000..2815cf9 --- /dev/null +++ b/tagoapi/utils/decorator/convert_model.py @@ -0,0 +1,48 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from tagoapi.utils.cache import cache, _generate_cache_key +from tagoapi.models.BaseList import BaseList + +if TYPE_CHECKING: + from tagoapi.models import BaseModel + + + + +# method에서만 사용할 함수 +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: + if use_model: + cache.save(model.key.generate_key(raw)) + cache.save(key, raw, ttl) + else: + raw = cached + if model: + ## convert list + if isinstance(raw.get("result"), list): + res = model.from_list(raw.get("result"), raw.get("citycode")) + else: + 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 + + + return raw + return inner + return decorator + diff --git a/tagoapi/utils/http.py b/tagoapi/utils/get.py similarity index 92% rename from tagoapi/utils/http.py rename to tagoapi/utils/get.py index 0b636b2..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() @@ -13,7 +14,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 +23,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/tagoapi/utils/get_station.py b/tagoapi/utils/get_station.py index 1414a73..1e3ed32 100644 --- a/tagoapi/utils/get_station.py +++ b/tagoapi/utils/get_station.py @@ -1,44 +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]: - 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("정류소 캐시파일을 불러오지 못했습니다.") +# # +# # +# # +# # 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 bd06611..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: - 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 diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..17be88b --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,81 @@ +import unittest +from unittest.mock import patch +from tagoapi import TAGOClient, TAGOAuth +from tagoapi.utils.cache import cache + +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) + + @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(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) + + @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) + print(cache.current_cache) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_get.py b/tests/test_get.py new file mode 100644 index 0000000..f4a1b6f --- /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="북구1", cityCode=22) + +# pprint(cache.current_cache) +print(route[0].stations) + diff --git a/tests/test_object_route.py b/tests/test_object_route.py new file mode 100644 index 0000000..06022d9 --- /dev/null +++ b/tests/test_object_route.py @@ -0,0 +1,81 @@ +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", cityCode=22) + 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", cityCode=22) + + # client_mock 생성 + mock_client = MagicMock() + 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_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) + + 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) + + 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) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_object_station.py b/tests/test_object_station.py new file mode 100644 index 0000000..f895eed --- /dev/null +++ b/tests/test_object_station.py @@ -0,0 +1,58 @@ +import copy +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_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 ) ======") + # + # + # 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 diff --git a/tests/test_station.py b/tests/test_station.py index 5592564..75d1aef 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -1,4 +1,13 @@ -from tagoapi import * +from tagoapi import TAGOClient, TAGOAuth, cache +import os +from dotenv import load_dotenv +load_dotenv() +key = os.environ.get("TAGO_API_KEY") +client = TAGOClient(TAGOAuth(key)) -get_station("adf") \ No newline at end of file +print(cache.current_cache) + +print(client.get_station(22, nodeNm="삼덕")) + +# print(get_station("대구소프트웨어마이스터고등학교"))