diff --git a/README.md b/README.md index e6744e7..63a509a 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Example: `http://127.0.0.1:8000/ocpi/docs/` - It's now possible to initialize a few versions of ocpi for one project; - Minimal required python version is 3.10; - Add cdrs module; + - Add tariffs module; ## Related diff --git a/py_ocpi/__init__.py b/py_ocpi/__init__.py index d1e96a7..0b4a057 100644 --- a/py_ocpi/__init__.py +++ b/py_ocpi/__init__.py @@ -1,6 +1,6 @@ """Python Implementation of OCPI""" -__version__ = "2023.09.28" +__version__ = "2023.10.02" from .core import enums, data_types from .main import get_application diff --git a/py_ocpi/core/endpoints/v_2_1_1/cpo.py b/py_ocpi/core/endpoints/v_2_1_1/cpo.py index a6f15ae..709730b 100644 --- a/py_ocpi/core/endpoints/v_2_1_1/cpo.py +++ b/py_ocpi/core/endpoints/v_2_1_1/cpo.py @@ -27,8 +27,14 @@ url=URL(f"{URL_BASE}/{ModuleID.cdrs.value}"), ) +TARIFFS = Endpoint( + identifier=ModuleID.tariffs, + url=URL(f"{URL_BASE}/{ModuleID.tariffs.value}"), +) + ENDPOINTS_LIST = { ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, ModuleID.locations: LOCATIONS, ModuleID.cdrs: CDRS, + ModuleID.tariffs: TARIFFS, } diff --git a/py_ocpi/core/endpoints/v_2_1_1/emsp.py b/py_ocpi/core/endpoints/v_2_1_1/emsp.py index 6272e20..c692b3b 100644 --- a/py_ocpi/core/endpoints/v_2_1_1/emsp.py +++ b/py_ocpi/core/endpoints/v_2_1_1/emsp.py @@ -27,8 +27,14 @@ url=URL(f"{URL_BASE}/{ModuleID.cdrs.value}"), ) +TARIFFS = Endpoint( + identifier=ModuleID.tariffs, + url=URL(f"{URL_BASE}/{ModuleID.tariffs.value}"), +) + ENDPOINTS_LIST = { ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, ModuleID.locations: LOCATIONS, ModuleID.cdrs: CDRS, + ModuleID.tariffs: TARIFFS, } diff --git a/py_ocpi/modules/tariffs/v_2_1_1/api/__init__.py b/py_ocpi/modules/tariffs/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/tariffs/v_2_1_1/api/__init__.py @@ -0,0 +1,2 @@ +from .cpo import router as cpo_router +from .emsp import router as emsp_router diff --git a/py_ocpi/modules/tariffs/v_2_1_1/api/cpo.py b/py_ocpi/modules/tariffs/v_2_1_1/api/cpo.py new file mode 100644 index 0000000..87ede84 --- /dev/null +++ b/py_ocpi/modules/tariffs/v_2_1_1/api/cpo.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Depends, Response, Request + +from py_ocpi.core.utils import get_list, get_auth_token_from_header +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.crud import Crud +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters +from py_ocpi.modules.versions.enums import VersionNumber + +router = APIRouter( + prefix="/tariffs", +) + + +@router.get("/", response_model=OCPIResponse) +async def get_tariffs( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + auth_token = get_auth_token_from_header(request) + + data_list = await get_list( + response, + filters, + ModuleID.tariffs, + RoleEnum.cpo, + VersionNumber.v_2_1_1, + crud, + auth_token=auth_token, + ) + + tariffs = [] + for data in data_list: + tariffs.append( + adapter.tariff_adapter(data, VersionNumber.v_2_1_1).dict() + ) + return OCPIResponse( + data=tariffs, + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/tariffs/v_2_1_1/api/emsp.py b/py_ocpi/modules/tariffs/v_2_1_1/api/emsp.py new file mode 100644 index 0000000..fffb658 --- /dev/null +++ b/py_ocpi/modules/tariffs/v_2_1_1/api/emsp.py @@ -0,0 +1,177 @@ +import copy + +from fastapi import APIRouter, Depends, Request + +from py_ocpi.core import status +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.crud import Crud +from py_ocpi.core.data_types import String +from py_ocpi.core.dependencies import get_crud, get_adapter +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.utils import ( + get_auth_token_from_header, + partially_update_attributes, +) +from py_ocpi.modules.tariffs.v_2_1_1.schemas import Tariff, TariffPartialUpdate +from py_ocpi.modules.versions.enums import VersionNumber + +router = APIRouter( + prefix="/tariffs", +) + + +@router.get( + "/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse +) +async def get_tariff( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + tariff_id: String(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + auth_token = get_auth_token_from_header(request) + + data = await crud.get( + ModuleID.tariffs, + RoleEnum.emsp, + tariff_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + return OCPIResponse( + data=[adapter.tariff_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.put( + "/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse +) +async def add_or_update_tariff( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + tariff_id: String(36), # type: ignore + tariff: Tariff, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + auth_token = get_auth_token_from_header(request) + + data = await crud.get( + ModuleID.tariffs, + RoleEnum.emsp, + tariff_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + data = await crud.update( + ModuleID.tariffs, + RoleEnum.emsp, + tariff.dict(), + tariff_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + else: + data = await crud.create( + ModuleID.tariffs, + RoleEnum.emsp, + tariff.dict(), + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[adapter.tariff_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.patch( + "/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse +) +async def partial_update_tariff( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + tariff_id: String(36), # type: ignore + tariff: TariffPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + auth_token = get_auth_token_from_header(request) + + old_data = await crud.get( + ModuleID.tariffs, + RoleEnum.emsp, + tariff_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + old_tariff = adapter.tariff_adapter(old_data, VersionNumber.v_2_1_1) + new_tariff = copy.deepcopy(old_tariff) + + partially_update_attributes( + new_tariff, tariff.dict(exclude_defaults=True, exclude_unset=True) + ) + + data = await crud.update( + ModuleID.tariffs, + RoleEnum.emsp, + new_tariff.dict(), + tariff_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[adapter.tariff_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.delete( + "/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse +) +async def delete_tariff( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + tariff_id: String(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + auth_token = get_auth_token_from_header(request) + + await crud.delete( + ModuleID.tariffs, + RoleEnum.emsp, + tariff_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/tariffs/v_2_1_1/schemas.py b/py_ocpi/modules/tariffs/v_2_1_1/schemas.py index 137c1d5..dbd32c5 100644 --- a/py_ocpi/modules/tariffs/v_2_1_1/schemas.py +++ b/py_ocpi/modules/tariffs/v_2_1_1/schemas.py @@ -59,3 +59,13 @@ class Tariff(BaseModel): elements: List[TariffElement] energy_mix: Optional[EnergyMix] last_updated: DateTime + + +class TariffPartialUpdate(BaseModel): + id: Optional[String(36)] # type: ignore + currency: Optional[String(3)] # type: ignore + tariff_alt_text: Optional[List[DisplayText]] + tariff_alt_url: Optional[URL] + elements: Optional[List[TariffElement]] + energy_mix: Optional[EnergyMix] + last_updated: Optional[DateTime] diff --git a/py_ocpi/routers/v_2_1_1/cpo.py b/py_ocpi/routers/v_2_1_1/cpo.py index 975ed8e..1fdd085 100644 --- a/py_ocpi/routers/v_2_1_1/cpo.py +++ b/py_ocpi/routers/v_2_1_1/cpo.py @@ -9,10 +9,13 @@ from py_ocpi.modules.cdrs.v_2_1_1.api import ( cpo_router as cdrs_cpo_2_1_1_router, ) - +from py_ocpi.modules.tariffs.v_2_1_1.api import ( + cpo_router as tariffs_cpo_2_1_1_router, +) router = { ModuleID.locations: locations_cpo_2_1_1_router, ModuleID.credentials_and_registration: credentials_cpo_2_1_1_router, ModuleID.cdrs: cdrs_cpo_2_1_1_router, + ModuleID.tariffs: tariffs_cpo_2_1_1_router, } diff --git a/py_ocpi/routers/v_2_1_1/emsp.py b/py_ocpi/routers/v_2_1_1/emsp.py index 760f23e..1da1a21 100644 --- a/py_ocpi/routers/v_2_1_1/emsp.py +++ b/py_ocpi/routers/v_2_1_1/emsp.py @@ -9,10 +9,13 @@ from py_ocpi.modules.cdrs.v_2_1_1.api import ( emsp_router as cdrs_emsp_2_1_1_router, ) - +from py_ocpi.modules.tariffs.v_2_1_1.api import ( + emsp_router as tariffs_emsp_2_1_1_router, +) router = { ModuleID.locations: locations_emsp_2_1_1_router, ModuleID.credentials_and_registration: credentials_emsp_2_1_1_router, ModuleID.cdrs: cdrs_emsp_2_1_1_router, + ModuleID.tariffs: tariffs_emsp_2_1_1_router, } diff --git a/tests/test_modules/test_v_2_1_1/test_tariffs.py b/tests/test_modules/test_v_2_1_1/test_tariffs.py new file mode 100644 index 0000000..bd6beb4 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tariffs.py @@ -0,0 +1,165 @@ +from uuid import uuid4 + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.core.config import settings +from py_ocpi.modules.versions.enums import VersionNumber + +TARIFFS = [ + { + "id": str(uuid4()), + "currency": "MYR", + "elements": [ + { + "price_components": [ + { + "type": "ENERGY", + "price": 1.50, + "step_size": 2, + }, + ] + }, + ], + "last_updated": "2022-01-02 00:00:00+00:00", + }, +] + + +class Crud: + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ): + return TARIFFS[0] + + @classmethod + async def update( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + id, + *args, + **kwargs, + ): + return data + + @classmethod + async def create( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + *args, + **kwargs, + ): + return data + + @classmethod + async def delete( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ): + ... + + @classmethod + async def list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return TARIFFS, 1, True + + +def test_cpo_get_tariffs_v_2_1_1(): + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + modules=[enums.ModuleID.tariffs], + ) + + client = TestClient(app) + response = client.get("/ocpi/cpo/2.1.1/tariffs") + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] + + +def test_emsp_get_tariff_v_2_1_1(): + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + modules=[enums.ModuleID.tariffs], + ) + + client = TestClient(app) + response = client.get( + f"/ocpi/emsp/2.1.1/tariffs/{settings.COUNTRY_CODE}/{settings.PARTY_ID}" + f'/{TARIFFS[0]["id"]}' + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] + + +def test_emsp_add_tariff_v_2_1_1(): + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + modules=[enums.ModuleID.tariffs], + ) + + client = TestClient(app) + response = client.put( + f"/ocpi/emsp/2.1.1/tariffs/{settings.COUNTRY_CODE}/{settings.PARTY_ID}" + f'/{TARIFFS[0]["id"]}', + json=TARIFFS[0], + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] + + +def test_emsp_patch_tariff_v_2_1_1(): + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + modules=[enums.ModuleID.tariffs], + ) + + patch_data = {"id": str(uuid4())} + client = TestClient(app) + response = client.patch( + f"/ocpi/emsp/2.1.1/tariffs/{settings.COUNTRY_CODE}/{settings.PARTY_ID}" + f'/{TARIFFS[0]["id"]}', + json=patch_data, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == patch_data["id"] + + +def test_emsp_delete_tariff_v_2_1_1(): + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + modules=[enums.ModuleID.tariffs], + ) + + client = TestClient(app) + response = client.delete( + f"/ocpi/emsp/2.1.1/tariffs/{settings.COUNTRY_CODE}/{settings.PARTY_ID}" + f'/{TARIFFS[0]["id"]}' + ) + + assert response.status_code == 200