| test_client = <httpx.AsyncClient object at 0x1064ee510>, app = <fastapi.applications.FastAPI object at 0x106280690>, mocker = <pytest_mock.plugin.MockerFixture object at 0x10660a820>
|
| station = ClassicServer(id='yc_station_67', branch_id='', vendor_id='vendor_yc', field_server_id='', station_title='測試站', descri...670': ClassicServerChargerMapItemValue(charger_id='bnl_ocppv16_smart12345670', power_type=None, connector_count=None)})
|
| charger = ClassicCharger(id='bnl_ocppv16_smart12345670', station_id='yc_station_67', charge_station='', name='測試樁')
|
|
|
| @pytest.mark.asyncio
|
| async def test_available(test_client: AsyncClient, app: FastAPI, mocker: MockerFixture, station: ClassicServer, charger: ClassicCharger):
|
| _set_auth_override(mocker)
|
| _set_redis_mock(app, mocker, _make_charger_info())
|
|
|
| app_state = cast(AppState, app.state)
|
| config_db = app_state['classic_config_db_engine']
|
| poi_db = app_state['classic_poi_db_engine']
|
|
|
| await config_db.save(station)
|
| await poi_db.save(charger)
|
|
|
| try:
|
| _set_fee_mock(mocker, fee=8.0, unit=FeeUnitEnum.FEE_UNIT_BY_KWH)
|
|
|
| > response = await test_client.get(f'/classic/chargers/{CHARGER_ID}')
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
| app/tests/test_100_classic_app/test_114_charger.py:142:
|
| _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
| .venv/lib/python3.13/site-packages/httpx/_client.py:1768: in get
|
| return await self.request(
|
| .venv/lib/python3.13/site-packages/httpx/_client.py:1540: in request
|
| return await self.send(request, auth=auth, follow_redirects=follow_redirects)
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| .venv/lib/python3.13/site-packages/httpx/_client.py:1629: in send
|
| response = await self._send_handling_auth(
|
| .venv/lib/python3.13/site-packages/httpx/_client.py:1657: in _send_handling_auth
|
| response = await self._send_handling_redirects(
|
| .venv/lib/python3.13/site-packages/httpx/_client.py:1694: in _send_handling_redirects
|
| response = await self._send_single_request(request)
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| .venv/lib/python3.13/site-packages/httpx/_client.py:1730: in _send_single_request
|
| response = await transport.handle_async_request(request)
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| .venv/lib/python3.13/site-packages/httpx/_transports/asgi.py:170: in handle_async_request
|
| await self.app(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__
|
| await super().__call__(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/applications.py:90: in __call__
|
| await self.middleware_stack(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__
|
| raise exc
|
| .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__
|
| await self.app(scope, receive, _send)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/base.py:198: in __call__
|
| raise app_exc
|
| .venv/lib/python3.13/site-packages/starlette/middleware/base.py:144: in coro
|
| await self.app(scope, receive_or_disconnect, send_no_error)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/cors.py:88: in __call__
|
| await self.app(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/gzip.py:29: in __call__
|
| await responder(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/gzip.py:130: in __call__
|
| await super().__call__(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/gzip.py:46: in __call__
|
| await self.app(scope, receive, self.send_with_compression)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__
|
| await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app
|
| raise exc
|
| .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app
|
| await app(scope, receive, sender)
|
| .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
|
| await self.app(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/routing.py:660: in __call__
|
| await self.middleware_stack(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:2683: in app
|
| await route.handle(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/routing.py:448: in handle
|
| await self.app(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__
|
| await super().__call__(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/applications.py:90: in __call__
|
| await self.middleware_stack(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__
|
| raise exc
|
| .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__
|
| await self.app(scope, receive, _send)
|
| .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__
|
| await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app
|
| raise exc
|
| .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app
|
| await app(scope, receive, sender)
|
| .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
|
| await self.app(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/routing.py:660: in __call__
|
| await self.middleware_stack(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:2683: in app
|
| await route.handle(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:1753: in handle
|
| await self.original_router.handle(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:2738: in handle
|
| await included_router._handle_selected(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:1773: in _handle_selected
|
| await original_route.handle(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:1264: in handle
|
| await app(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:150: in app
|
| await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
| .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app
|
| raise exc
|
| .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app
|
| await app(scope, receive, sender)
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:136: in app
|
| response = await f(request)
|
| ^^^^^^^^^^^^^^^^
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:690: in app
|
| raw_response = await run_endpoint_function(
|
| .venv/lib/python3.13/site-packages/fastapi/routing.py:344: in run_endpoint_function
|
| return await dependant.call(**values)
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
|
|
| charger_id = 'bnl_ocppv16_smart12345670', auth_result = {'error_code': <ClassicErrorCodeEnum.ERROR_OK: 0>, 'member': <MagicMock id='4401383808'>}
|
| app_state = <starlette.datastructures.State object at 0x105540d70>, connector_id = 1
|
|
|
| @router.get('/{charger_id}', status_code=status.HTTP_200_OK, operation_id='get_charger')
|
| async def get_charger(
|
| charger_id: str,
|
| auth_result: Annotated[AuthMemberResult, Security(get_authed_and_active_member_di)],
|
| app_state: Annotated[AppState, Depends(get_app_state_di)],
|
| connector_id: int = Query(1, description='Connector ID'),
|
| ) -> GetChargerResponse:
|
| '''
|
| 復刻自 new-home GET /api/chargers/{charge_id}
|
|
|
| - 原本 new-home 的 charger ID 在路徑最後一個 segment,這裡採用 path parameter。
|
| - 成功回應沿用 new-home 慣例之 200。
|
| - 錯誤也沿用 new-home 慣例之 200,不用 4xx、5xx。
|
| - 回應之 `site_info.name` 統一命名為格式 {站名}-{樁名},如果兩個都沒有,就是 unnamed-unnamed。
|
|
|
| 費率方面:
|
| - 原本 new-home 有針對 northsungreen 站做特案處理,這裏沒有。
|
| - 原本 new-home 繞大圈跑層層函式在 Redis 與 DB 抓 smart charging configuration 然後再組一大堆 TOU slot 費率資料,這裏化繁為簡,只從 DB 抓當站當下 smart charging 費率,然後判斷 rate type。
|
| '''
|
| auth_result_code = auth_result['error_code']
|
| if auth_result_code != ClassicErrorCodeEnum.ERROR_OK:
|
| return GetChargerResponse(code=auth_result_code)
|
|
|
| redis_db0_client = app_state['classic_redis_clients']['db0_client']
|
| try: charger_info_bytes = cast(bytes | None, redis_db0_client.hget(RedisDb0HashNameEnum.CHARGER_INFO.value, charger_id))
|
| except Exception as e:
|
| logger.error({'title': 'Read charger info from Redis error', 'charger_id': charger_id, 'exception': repr(e)})
|
| return GetChargerResponse( # 沿用 new-home 回應,但這不合理,應該是 `ERROR_CHARGER_NO_EXIST` 或 `ERROR_CHARGER_DISCONNECTED` 較合理。
|
| code=ClassicErrorCodeEnum.ERROR_OK,
|
| available=False,
|
| site_info=ChargerSiteInfo(name='unnamed', unit='', rate=0),
|
| )
|
|
|
| > charger_info: dict | None = json.loads(charger_info_bytes.decode()) if charger_info_bytes else None
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
| E AttributeError: 'str' object has no attribute 'decode'. Did you mean: 'encode'?
|
|
|
| app/classic_app/routers/charger_router.py:58: AttributeError
|