From 770fd1bb666b9d316f0b1690fc3a90dd8ba8bc67 Mon Sep 17 00:00:00 2001 From: Hamed Akhavan <61178707+HamedAkhavan@users.noreply.github.com> Date: Sun, 16 Apr 2023 11:18:34 +0330 Subject: [PATCH] Version 0.3.1 (#49) * feat: add basic structure * feat: add few ocpi data types * feat: add data types * feat: add version module enums and schemas * feat: add basic versions module * feat: add versions details endpoint * refactor: modify VersionNumber enum * feat: add locations module, crud and adaptor * chore: update prospector config * refactor: adaptor * refactor: adaptor * chore: update readme * feat: make crud and adapter dependency, add example * refactor: import core enums and datatypes to source module * refactor: make routers and CRUD methods asycn * fix: linter * fix: update schema definitions * fix: update schema definitions * chore: add pypi publish workflow * refactor: change project name to py_ocpi * Update README.md * refactor: get prefix from env * refactor: change HOST to OCPI_HOST * feat: add pagination for list endpoints * fix: add id param to update method of crud * Update README.md * Rename README.md to README.rst * Update README.rst * Update pyproject.toml * OCPI-55 (#7) * feat: add sessions module schemas and enums * feat: add sessions module schemas and enums * feat: add sessions endpoints * OCPI-48 (#3) * feat: add schemas and enums for credentials module * fix: pylint fix * feat: add credentials module * fix: PR review changes * feat: make crud funtions in credentials endpoint async * fix: change verification params * feat: add cdrs schemas and enums (#10) * feat: add cdrs endpoints, refactor code enums, move universal files to core (#11) * OCPI-48 (#15) * feat: update authorization process * fix: pylint fix * feat: add exception handlder * fix: pylint, add middleware fix * feat: add all test modules (#17) * feat: change post credentials url * feat: modify commands module cpo (#18) * OCPI-66 (#22) * feat: improve crud and adapter, improve OCPI version management * refactor: add routers and modules directories * Fix the router URLs (#36) * Fix the router URLs * Fix versions and details URLs * fix: return version details based on role (#39) * Use dict type instead of list for details endpoint (#40) * Fix the initial authentication (#43) * Base64 encode the bearer token * Decode the base64 encoded authentication token * Require authorization when listing versions * Revert "Fix the initial authentication (#43)" (#44) This reverts commit a40dc0def8977885e178f5d8ca450bdbecc476b2. * Update push.yaml * Fix initial credentials exchange (#46) * Fix the initial authentication (#43) * Base64 encode the bearer token * Decode the base64 encoded authentication token * Require authorization when listing versions * Remove the list wrapping the credentials object * Fix tests * Skip false positive Bandit test B105 * Ignore false positive * Fix linter error * feat: make new version 0.3.1 --------- Co-authored-by: Hamed Akhavan <61178707+HAkhavan71@users.noreply.github.com> Co-authored-by: ViCt0r99 <87501969+ViCt0r99@users.noreply.github.com> Co-authored-by: ViCt0r99 Co-authored-by: Wojtek Siudzinski --- .github/workflows/push.yaml | 1 + py_ocpi/__init__.py | 2 +- py_ocpi/core/dependencies.py | 6 +- py_ocpi/core/endpoints.py | 195 +++++++++--------- py_ocpi/core/push.py | 4 +- py_ocpi/core/schemas.py | 4 +- py_ocpi/core/utils.py | 16 +- py_ocpi/main.py | 38 ++-- py_ocpi/modules/cdrs/v_2_2_1/schemas.py | 2 +- py_ocpi/modules/commands/v_2_2_1/api/cpo.py | 4 +- .../modules/credentials/v_2_2_1/api/cpo.py | 14 +- .../modules/credentials/v_2_2_1/api/emsp.py | 14 +- py_ocpi/modules/versions/api/v_2_2_1.py | 28 ++- pyproject.toml | 2 +- tests/test_modules/mocks/async_client.py | 3 +- tests/test_modules/test_cdrs.py | 2 +- tests/test_modules/test_commands.py | 11 +- tests/test_modules/test_credentials.py | 22 +- tests/test_modules/test_sessions.py | 2 +- tests/test_modules/test_versions.py | 29 ++- 20 files changed, 230 insertions(+), 169 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 289e982..c0b9f4c 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -5,6 +5,7 @@ on: branches: - 'develop' - 'feature/**' + - 'fix/**' jobs: lint: diff --git a/py_ocpi/__init__.py b/py_ocpi/__init__.py index 731d704..e1a2346 100644 --- a/py_ocpi/__init__.py +++ b/py_ocpi/__init__.py @@ -1,6 +1,6 @@ """Python Implementation of OCPI""" -__version__ = "0.3.0" +__version__ = "0.3.1" from .core import enums, data_types from .main import get_application diff --git a/py_ocpi/core/dependencies.py b/py_ocpi/core/dependencies.py index 7d9b316..9904afd 100644 --- a/py_ocpi/core/dependencies.py +++ b/py_ocpi/core/dependencies.py @@ -22,11 +22,15 @@ def get_versions(): return [ Version( version=VersionNumber.v_2_2_1, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/{VersionNumber.v_2_2_1}/details') + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/{VersionNumber.v_2_2_1.value}/details') ).dict(), ] +def get_endpoints(): + return {} + + def pagination_filters( date_from: datetime = Query(default=None), date_to: datetime = Query(default=datetime.now()), diff --git a/py_ocpi/core/endpoints.py b/py_ocpi/core/endpoints.py index e320312..6ec48ab 100644 --- a/py_ocpi/core/endpoints.py +++ b/py_ocpi/core/endpoints.py @@ -1,107 +1,108 @@ -from py_ocpi.core.enums import ModuleID +from py_ocpi.core.enums import ModuleID, RoleEnum from py_ocpi.core.data_types import URL from py_ocpi.core.config import settings from py_ocpi.modules.versions.schemas import Endpoint from py_ocpi.modules.versions.enums import VersionNumber, InterfaceRole ENDPOINTS = { - VersionNumber.v_2_2_1: [ + VersionNumber.v_2_2_1: { # ###############--CPO--############### - - # locations - Endpoint( - identifier=ModuleID.locations, - role=InterfaceRole.sender, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' - f'/{VersionNumber.v_2_2_1}/{ModuleID.locations}') - ), - # sessions - Endpoint( - identifier=ModuleID.sessions, - role=InterfaceRole.sender, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' - f'/{VersionNumber.v_2_2_1}/{ModuleID.sessions}') - ), - # credentials - Endpoint( - identifier=ModuleID.credentials_and_registration, - role=InterfaceRole.receiver, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' - f'/{VersionNumber.v_2_2_1}/{ModuleID.credentials_and_registration}') - ), - # tariffs - Endpoint( - identifier=ModuleID.tariffs, - role=InterfaceRole.sender, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' - f'/{VersionNumber.v_2_2_1}/{ModuleID.tariffs}') - ), - # cdrs - Endpoint( - identifier=ModuleID.cdrs, - role=InterfaceRole.sender, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' - f'/{VersionNumber.v_2_2_1}/{ModuleID.cdrs}') - ), - # tokens - Endpoint( - identifier=ModuleID.tokens, - role=InterfaceRole.receiver, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' - f'/{VersionNumber.v_2_2_1}/{ModuleID.tokens}') - ), + RoleEnum.cpo: [ + # locations + Endpoint( + identifier=ModuleID.locations, + role=InterfaceRole.sender, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.locations.value}') + ), + # sessions + Endpoint( + identifier=ModuleID.sessions, + role=InterfaceRole.sender, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.sessions.value}') + ), + # credentials + Endpoint( + identifier=ModuleID.credentials_and_registration, + role=InterfaceRole.receiver, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.credentials_and_registration.value}') + ), + # tariffs + Endpoint( + identifier=ModuleID.tariffs, + role=InterfaceRole.sender, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.tariffs.value}') + ), + # cdrs + Endpoint( + identifier=ModuleID.cdrs, + role=InterfaceRole.sender, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.cdrs.value}') + ), + # tokens + Endpoint( + identifier=ModuleID.tokens, + role=InterfaceRole.receiver, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.tokens.value}') + ), + ], # ###############--EMSP--############### - - # credentials - Endpoint( - identifier=ModuleID.credentials_and_registration, - role=InterfaceRole.receiver, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.credentials_and_registration}') - ), - # locations - Endpoint( - identifier=ModuleID.locations, - role=InterfaceRole.receiver, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.locations}') - ), - # sessions - Endpoint( - identifier=ModuleID.sessions, - role=InterfaceRole.receiver, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.sessions}') - ), - # cdrs - Endpoint( - identifier=ModuleID.cdrs, - role=InterfaceRole.receiver, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.cdrs}') - ), - # tariffs - Endpoint( - identifier=ModuleID.tariffs, - role=InterfaceRole.receiver, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.tariffs}') - ), - # commands - Endpoint( - identifier=ModuleID.commands, - role=InterfaceRole.sender, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.commands}') - ), - # tokens - Endpoint( - identifier=ModuleID.tokens, - role=InterfaceRole.sender, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.tokens}') - ), - ] - + RoleEnum.emsp: [ + # credentials + Endpoint( + identifier=ModuleID.credentials_and_registration, + role=InterfaceRole.receiver, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.credentials_and_registration.value}') + ), + # locations + Endpoint( + identifier=ModuleID.locations, + role=InterfaceRole.receiver, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.locations.value}') + ), + # sessions + Endpoint( + identifier=ModuleID.sessions, + role=InterfaceRole.receiver, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.sessions.value}') + ), + # cdrs + Endpoint( + identifier=ModuleID.cdrs, + role=InterfaceRole.receiver, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.cdrs.value}') + ), + # tariffs + Endpoint( + identifier=ModuleID.tariffs, + role=InterfaceRole.receiver, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.tariffs.value}') + ), + # commands + Endpoint( + identifier=ModuleID.commands, + role=InterfaceRole.sender, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.commands.value}') + ), + # tokens + Endpoint( + identifier=ModuleID.tokens, + role=InterfaceRole.sender, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' + f'/{VersionNumber.v_2_2_1.value}/{ModuleID.tokens.value}') + ), + ] + } } diff --git a/py_ocpi/core/push.py b/py_ocpi/core/push.py index 553e35d..f7639a3 100644 --- a/py_ocpi/core/push.py +++ b/py_ocpi/core/push.py @@ -4,7 +4,7 @@ from py_ocpi.core.adapter import Adapter from py_ocpi.core.crud import Crud from py_ocpi.core.schemas import Push, PushResponse, ReceiverResponse -from py_ocpi.core.utils import get_auth_token +from py_ocpi.core.utils import encode_string_base64, get_auth_token from py_ocpi.core.dependencies import get_crud, get_adapter from py_ocpi.core.enums import ModuleID, RoleEnum from py_ocpi.core.config import settings @@ -66,7 +66,7 @@ async def push_object(version: VersionNumber, push: Push, crud: Crud, adapter: A receiver_responses = [] for receiver in push.receivers: # get client endpoints - client_auth_token = f'Token {receiver.auth_token}' + client_auth_token = f'Token {encode_string_base64(receiver.auth_token)}' async with httpx.AsyncClient() as client: response = await client.get(receiver.endpoints_url, headers={'authorization': client_auth_token}) diff --git a/py_ocpi/core/schemas.py b/py_ocpi/core/schemas.py index 548caed..754110a 100644 --- a/py_ocpi/core/schemas.py +++ b/py_ocpi/core/schemas.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from typing import List +from typing import List, Union from pydantic import BaseModel @@ -11,7 +11,7 @@ class OCPIResponse(BaseModel): """ https://github.com/ocpi/ocpi/blob/2.2.1/transport_and_format.asciidoc#117-response-format """ - data: list + data: Union[list, dict] status_code: int status_message: String(255) timestamp: DateTime = str(datetime.now(timezone.utc)) diff --git a/py_ocpi/core/utils.py b/py_ocpi/core/utils.py index 4276580..6655490 100644 --- a/py_ocpi/core/utils.py +++ b/py_ocpi/core/utils.py @@ -1,4 +1,5 @@ import urllib +import base64 from fastapi import Response, Request from pydantic import BaseModel @@ -18,7 +19,10 @@ def set_pagination_headers(response: Response, link: str, total: int, limit: int def get_auth_token(request: Request) -> str: headers = request.headers headers_token = headers.get('authorization', 'Token Null') - return headers_token.split()[1] + token = headers_token.split()[1] + if token == 'Null': # nosec + return None + return decode_string_base64(token) async def get_list(response: Response, filters: dict, module: ModuleID, role: RoleEnum, @@ -40,3 +44,13 @@ async def get_list(response: Response, filters: dict, module: ModuleID, role: Ro def partially_update_attributes(instance: BaseModel, attributes: dict): for key, value in attributes.items(): setattr(instance, key, value) + + +def encode_string_base64(input: str) -> str: + input_bytes = base64.b64encode(bytes(input, 'utf-8')) + return input_bytes.decode('utf-8') + + +def decode_string_base64(input: str) -> str: + input_bytes = base64.b64decode(bytes(input, 'utf-8')) + return input_bytes.decode('utf-8') diff --git a/py_ocpi/main.py b/py_ocpi/main.py index a4f5e08..8889b9a 100644 --- a/py_ocpi/main.py +++ b/py_ocpi/main.py @@ -5,11 +5,12 @@ from fastapi.middleware.cors import CORSMiddleware from pydantic import ValidationError from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from py_ocpi.core.endpoints import ENDPOINTS from py_ocpi.modules.versions.api import router as versions_router, versions_v_2_2_1_router from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.modules.versions.schemas import Version -from py_ocpi.core.dependencies import get_crud, get_adapter, get_versions +from py_ocpi.core.dependencies import get_crud, get_adapter, get_versions, get_endpoints from py_ocpi.core import status from py_ocpi.core.enums import RoleEnum from py_ocpi.core.config import settings @@ -82,6 +83,7 @@ def get_application( ) versions = [] + version_endpoints = {} if VersionNumber.v_2_2_1 in version_numbers: _app.include_router( @@ -89,33 +91,30 @@ def get_application( prefix=f'/{settings.OCPI_PREFIX}', ) + versions.append( + Version( + version=VersionNumber.v_2_2_1, + url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/{VersionNumber.v_2_2_1.value}/details') + ).dict(), + ) + + version_endpoints[VersionNumber.v_2_2_1] = [] + if RoleEnum.cpo in roles: _app.include_router( v_2_2_1_cpo_router, - prefix=f'/{settings.OCPI_PREFIX}/cpo/{VersionNumber.v_2_2_1}', + prefix=f'/{settings.OCPI_PREFIX}/cpo/{VersionNumber.v_2_2_1.value}', tags=['CPO'] ) - - versions.append( - Version( - version=VersionNumber.v_2_2_1, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/cpo/{VersionNumber.v_2_2_1}') - ).dict(), - ) + version_endpoints[VersionNumber.v_2_2_1] += ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.cpo] if RoleEnum.emsp in roles: _app.include_router( v_2_2_1_emsp_router, - prefix=f'/{settings.OCPI_PREFIX}/emsp/{VersionNumber.v_2_2_1}', + prefix=f'/{settings.OCPI_PREFIX}/emsp/{VersionNumber.v_2_2_1.value}', tags=['EMSP'] ) - - versions.append( - Version( - version=VersionNumber.v_2_2_1, - url=URL(f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp/{VersionNumber.v_2_2_1}') - ).dict(), - ) + version_endpoints[VersionNumber.v_2_2_1] += ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.emsp] def override_get_crud(): return crud @@ -132,4 +131,9 @@ def override_get_versions(): _app.dependency_overrides[get_versions] = override_get_versions + def override_get_endpoints(): + return version_endpoints + + _app.dependency_overrides[get_endpoints] = override_get_endpoints + return _app diff --git a/py_ocpi/modules/cdrs/v_2_2_1/schemas.py b/py_ocpi/modules/cdrs/v_2_2_1/schemas.py index c18bb3b..704f171 100644 --- a/py_ocpi/modules/cdrs/v_2_2_1/schemas.py +++ b/py_ocpi/modules/cdrs/v_2_2_1/schemas.py @@ -43,7 +43,7 @@ class ChargingPeriod(BaseModel): https://github.com/ocpi/ocpi/blob/2.2.1/mod_cdrs.asciidoc#146-chargingperiod-class """ start_date_time: DateTime - diemnsions: List[CdrDimension] + dimensions: List[CdrDimension] tariff_id: Optional[CiString(36)] diff --git a/py_ocpi/modules/commands/v_2_2_1/api/cpo.py b/py_ocpi/modules/commands/v_2_2_1/api/cpo.py index 72a3816..bb16333 100644 --- a/py_ocpi/modules/commands/v_2_2_1/api/cpo.py +++ b/py_ocpi/modules/commands/v_2_2_1/api/cpo.py @@ -13,7 +13,7 @@ from py_ocpi.core.adapter import Adapter from py_ocpi.core.crud import Crud from py_ocpi.core import status -from py_ocpi.core.utils import get_auth_token +from py_ocpi.core.utils import encode_string_base64, get_auth_token from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.modules.commands.v_2_2_1.enums import CommandType from py_ocpi.modules.commands.v_2_2_1.schemas import ( @@ -59,7 +59,7 @@ async def send_command_result(response_url: str, command: CommandType, auth_toke command_result = adapter.command_result_adapter(command_result, VersionNumber.v_2_2_1) async with httpx.AsyncClient() as client: - authorization_token = f'Token {client_auth_token}' + authorization_token = f'Token {encode_string_base64(client_auth_token)}' await client.post(response_url, json=command_result.dict(), headers={'authorization': authorization_token}) diff --git a/py_ocpi/modules/credentials/v_2_2_1/api/cpo.py b/py_ocpi/modules/credentials/v_2_2_1/api/cpo.py index 4697711..06d869d 100644 --- a/py_ocpi/modules/credentials/v_2_2_1/api/cpo.py +++ b/py_ocpi/modules/credentials/v_2_2_1/api/cpo.py @@ -5,7 +5,7 @@ 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.utils import get_auth_token +from py_ocpi.core.utils import encode_string_base64, get_auth_token from py_ocpi.core.dependencies import get_crud, get_adapter from py_ocpi.core import status from py_ocpi.core.enums import Action, ModuleID, RoleEnum @@ -24,7 +24,7 @@ async def get_credentials(request: Request, crud: Crud = Depends(get_crud), adap data = await crud.get(ModuleID.credentials_and_registration, RoleEnum.cpo, auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) return OCPIResponse( - data=[adapter.credentials_adapter(data).dict()], + data=adapter.credentials_adapter(data).dict(), **status.OCPI_1000_GENERIC_SUCESS_CODE, ) @@ -43,7 +43,7 @@ async def post_credentials(request: Request, credentials: Credentials, # Retrieve the versions and endpoints from the client async with httpx.AsyncClient() as client: - authorization_token = f'Token {credentials_client_token}' + authorization_token = f'Token {encode_string_base64(credentials_client_token)}' response_versions = await client.get(credentials.url, headers={'authorization': authorization_token}) @@ -66,7 +66,7 @@ async def post_credentials(request: Request, credentials: Credentials, if response_endpoints.status_code == fastapistatus.HTTP_200_OK: # Store client credentials and generate new credentials for sender - endpoints = response_endpoints.json()['data'][0] + endpoints = response_endpoints.json()['data'] new_credentials = await crud.create( ModuleID.credentials_and_registration, RoleEnum.cpo, { @@ -78,7 +78,7 @@ async def post_credentials(request: Request, credentials: Credentials, ) return OCPIResponse( - data=[adapter.credentials_adapter(new_credentials).dict()], + data=adapter.credentials_adapter(new_credentials).dict(), **status.OCPI_1000_GENERIC_SUCESS_CODE ) @@ -102,7 +102,7 @@ async def update_credentials(request: Request, credentials: Credentials, # Retrieve the versions and endpoints from the client async with httpx.AsyncClient() as client: - authorization_token = f'Token {credentials_client_token}' + authorization_token = f'Token {encode_string_base64(credentials_client_token)}' response_versions = await client.get(credentials.url, headers={'authorization': authorization_token}) if response_versions.status_code == fastapistatus.HTTP_200_OK: @@ -134,7 +134,7 @@ async def update_credentials(request: Request, credentials: Credentials, version=VersionNumber.v_2_2_1) return OCPIResponse( - data=[adapter.credentials_adapter(new_credentials).dict()], + data=adapter.credentials_adapter(new_credentials).dict(), **status.OCPI_1000_GENERIC_SUCESS_CODE ) diff --git a/py_ocpi/modules/credentials/v_2_2_1/api/emsp.py b/py_ocpi/modules/credentials/v_2_2_1/api/emsp.py index 755aa90..e713e6e 100644 --- a/py_ocpi/modules/credentials/v_2_2_1/api/emsp.py +++ b/py_ocpi/modules/credentials/v_2_2_1/api/emsp.py @@ -5,7 +5,7 @@ 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.utils import get_auth_token +from py_ocpi.core.utils import encode_string_base64, get_auth_token from py_ocpi.core.dependencies import get_crud, get_adapter from py_ocpi.core import status from py_ocpi.core.enums import Action, ModuleID, RoleEnum @@ -24,7 +24,7 @@ async def get_credentials(request: Request, crud: Crud = Depends(get_crud), adap data = await crud.get(ModuleID.credentials_and_registration, RoleEnum.emsp, auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) return OCPIResponse( - data=[adapter.credentials_adapter(data).dict()], + data=adapter.credentials_adapter(data).dict(), **status.OCPI_1000_GENERIC_SUCESS_CODE, ) @@ -43,7 +43,7 @@ async def post_credentials(request: Request, credentials: Credentials, # Retrieve the versions and endpoints from the client async with httpx.AsyncClient() as client: - authorization_token = f'Token {credentials_client_token}' + authorization_token = f'Token {encode_string_base64(credentials_client_token)}' response_versions = await client.get(credentials.url, headers={'authorization': authorization_token}) @@ -66,7 +66,7 @@ async def post_credentials(request: Request, credentials: Credentials, if response_endpoints.status_code == fastapistatus.HTTP_200_OK: # Store client credentials and generate new credentials for sender - endpoints = response_endpoints.json()['data'][0] + endpoints = response_endpoints.json()['data'] new_credentials = await crud.create( ModuleID.credentials_and_registration, RoleEnum.emsp, { @@ -78,7 +78,7 @@ async def post_credentials(request: Request, credentials: Credentials, ) return OCPIResponse( - data=[adapter.credentials_adapter(new_credentials).dict()], + data=adapter.credentials_adapter(new_credentials).dict(), **status.OCPI_1000_GENERIC_SUCESS_CODE ) @@ -102,7 +102,7 @@ async def update_credentials(request: Request, credentials: Credentials, # Retrieve the versions and endpoints from the client async with httpx.AsyncClient() as client: - authorization_token = f'Token {credentials_client_token}' + authorization_token = f'Token {encode_string_base64(credentials_client_token)}' response_versions = await client.get(credentials.url, headers={'authorization': authorization_token}) if response_versions.status_code == fastapistatus.HTTP_200_OK: @@ -134,7 +134,7 @@ async def update_credentials(request: Request, credentials: Credentials, version=VersionNumber.v_2_2_1) return OCPIResponse( - data=[adapter.credentials_adapter(new_credentials).dict()], + data=adapter.credentials_adapter(new_credentials).dict(), **status.OCPI_1000_GENERIC_SUCESS_CODE ) diff --git a/py_ocpi/modules/versions/api/v_2_2_1.py b/py_ocpi/modules/versions/api/v_2_2_1.py index e99a58b..158f2cc 100644 --- a/py_ocpi/modules/versions/api/v_2_2_1.py +++ b/py_ocpi/modules/versions/api/v_2_2_1.py @@ -1,22 +1,30 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends, Request, HTTPException, status as fastapistatus from py_ocpi.modules.versions.schemas import VersionDetail from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.crud import Crud from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse -from py_ocpi.core.endpoints import ENDPOINTS - +from py_ocpi.core.dependencies import get_endpoints, get_crud +from py_ocpi.core.utils import get_auth_token +from py_ocpi.core.enums import Action, ModuleID router = APIRouter() @router.get("/2.2.1/details", response_model=OCPIResponse) -async def get_version_details(): +async def get_version_details(request: Request, endpoints=Depends(get_endpoints), + crud: Crud = Depends(get_crud)): + auth_token = get_auth_token(request) + + server_cred = await crud.do(ModuleID.credentials_and_registration, None, Action.get_client_token, + auth_token=auth_token) + if server_cred is None: + raise HTTPException(fastapistatus.HTTP_401_UNAUTHORIZED, "Unauthorized") + return OCPIResponse( - data=[ - VersionDetail( - version=VersionNumber.v_2_2_1, - endpoints=ENDPOINTS[VersionNumber.v_2_2_1] - ).dict(), - ], + data=VersionDetail( + version=VersionNumber.v_2_2_1, + endpoints=endpoints[VersionNumber.v_2_2_1] + ).dict(), **status.OCPI_1000_GENERIC_SUCESS_CODE, ) diff --git a/pyproject.toml b/pyproject.toml index d278e8e..0c1ab73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "py_ocpi" -version = "0.3.0" +version = "0.3.1" description = "Python Implementation of OCPI" authors = ["HAkhavan71 "] readme = "README.rst" diff --git a/tests/test_modules/mocks/async_client.py b/tests/test_modules/mocks/async_client.py index d96cf79..4d64f29 100644 --- a/tests/test_modules/mocks/async_client.py +++ b/tests/test_modules/mocks/async_client.py @@ -1,5 +1,6 @@ from py_ocpi.core.dependencies import get_versions from py_ocpi.core.endpoints import ENDPOINTS +from py_ocpi.core.enums import RoleEnum from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.modules.versions.schemas import VersionDetail @@ -7,7 +8,7 @@ 'data': [ VersionDetail( version=VersionNumber.v_2_2_1, - endpoints=ENDPOINTS[VersionNumber.v_2_2_1] + endpoints=ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.cpo] ).dict(), ], } diff --git a/tests/test_modules/test_cdrs.py b/tests/test_modules/test_cdrs.py index 34eaf72..015d73e 100644 --- a/tests/test_modules/test_cdrs.py +++ b/tests/test_modules/test_cdrs.py @@ -46,7 +46,7 @@ 'charging_periods': [ { 'start_date_time': '2022-01-02 00:00:00+00:00', - 'diemnsions': [ + 'dimensions': [ { 'type': CdrDimensionType.power, 'volume': 10 diff --git a/tests/test_modules/test_commands.py b/tests/test_modules/test_commands.py index fcba2ac..4ddf0e1 100644 --- a/tests/test_modules/test_commands.py +++ b/tests/test_modules/test_commands.py @@ -26,6 +26,9 @@ class Crud: @classmethod async def do(cls, module: enums.ModuleID, role: enums.RoleEnum, action: enums.Action, *args, data: dict = None, **kwargs) -> dict: + if action == enums.Action.get_client_token: + return 'foo' + return COMMAND_RESPONSE @classmethod @@ -68,7 +71,7 @@ def test_cpo_receive_command_start_session_v_2_2_1(): } client = TestClient(app) - response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.start_session}', json=data) + response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.start_session.value}', json=data) assert response.status_code == 200 assert len(response.json()['data']) == 1 @@ -84,7 +87,7 @@ def test_cpo_receive_command_stop_session_v_2_2_1(): } client = TestClient(app) - response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.stop_session}', json=data) + response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.stop_session.value}', json=data) assert response.status_code == 200 assert len(response.json()['data']) == 1 @@ -114,7 +117,7 @@ def test_cpo_receive_command_reserve_now_v_2_2_1(): } client = TestClient(app) - response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.reserve_now}', json=data) + response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.reserve_now.value}', json=data) assert response.status_code == 200 assert len(response.json()['data']) == 1 @@ -154,7 +157,7 @@ async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kw } client = TestClient(app) - response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.reserve_now}', json=data) + response = client.post(f'/ocpi/cpo/2.2.1/commands/{CommandType.reserve_now.value}', json=data) assert response.status_code == 200 assert response.json()['data'][0]['result'] == CommandResultType.rejected diff --git a/tests/test_modules/test_credentials.py b/tests/test_modules/test_credentials.py index d550af6..d11c51b 100644 --- a/tests/test_modules/test_credentials.py +++ b/tests/test_modules/test_credentials.py @@ -1,6 +1,7 @@ import functools from uuid import uuid4 from unittest.mock import patch +from typing import Any import pytest from fastapi.testclient import TestClient @@ -11,6 +12,7 @@ from py_ocpi.core.data_types import URL from py_ocpi.core.config import settings from py_ocpi.core.dependencies import get_versions +from py_ocpi.core.utils import encode_string_base64 from py_ocpi.modules.credentials.v_2_2_1.schemas import Credentials from py_ocpi.modules.tokens.v_2_2_1.enums import AllowedType from py_ocpi.modules.tokens.v_2_2_1.schemas import AuthorizationInfo, Token @@ -80,27 +82,31 @@ def test_cpo_get_credentials_v_2_2_1(): app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) token = str(uuid4()) header = { - "Authorization": f'Token {token}' + "Authorization": f'Token {encode_string_base64(token)}' } client = TestClient(app) response = client.get('/ocpi/cpo/2.2.1/credentials', headers=header) assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['token'] == token + assert response.json()['data']['token'] == token @pytest.mark.asyncio @patch('py_ocpi.modules.credentials.v_2_2_1.api.cpo.httpx.AsyncClient') async def test_cpo_post_credentials_v_2_2_1(async_client): - app_1 = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) + class MockCrud(Crud): + @classmethod + async def do(cls, module: enums.ModuleID, role: enums.RoleEnum, action: enums.Action, auth_token, *args, data: dict = None, **kwargs) -> Any: + return {} + + app_1 = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], MockCrud, Adapter) def override_get_versions(): return [ Version( version=VersionNumber.v_2_2_1, - url=URL(f'/{settings.OCPI_PREFIX}/{VersionNumber.v_2_2_1}/details') + url=URL(f'/{settings.OCPI_PREFIX}/{VersionNumber.v_2_2_1.value}/details') ).dict() ] @@ -108,11 +114,11 @@ def override_get_versions(): async_client.return_value = AsyncClient(app=app_1, base_url="http://test") - app_2 = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) + + app_2 = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], MockCrud, Adapter) async with AsyncClient(app=app_2, base_url="http://test") as client: response = await client.post('/ocpi/cpo/2.2.1/credentials/', json=CREDENTIALS_TOKEN_CREATE) assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['token'] == CREDENTIALS_TOKEN_CREATE['token'] + assert response.json()['data']['token'] == CREDENTIALS_TOKEN_CREATE['token'] diff --git a/tests/test_modules/test_sessions.py b/tests/test_modules/test_sessions.py index b9c5915..9f51b47 100644 --- a/tests/test_modules/test_sessions.py +++ b/tests/test_modules/test_sessions.py @@ -34,7 +34,7 @@ 'charging_periods': [ { 'start_date_time': '2022-01-02 00:00:00+00:00', - 'diemnsions': [ + 'dimensions': [ { 'type': CdrDimensionType.power, 'volume': 10 diff --git a/tests/test_modules/test_versions.py b/tests/test_modules/test_versions.py index 40765f9..25dde7b 100644 --- a/tests/test_modules/test_versions.py +++ b/tests/test_modules/test_versions.py @@ -1,14 +1,13 @@ -from uuid import uuid4 +from typing import Any 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.core.crud import Crud from py_ocpi.core.adapter import Adapter -from py_ocpi.modules.locations.v_2_2_1.schemas import Location from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.enums import ModuleID, RoleEnum, Action def test_get_versions(): @@ -23,11 +22,31 @@ def test_get_versions(): def test_get_versions_v_2_2_1(): + token = None + class MockCrud(Crud): + @classmethod + async def do(cls, module: ModuleID, role: RoleEnum, action: Action, auth_token, *args, data: dict = None, **kwargs) -> Any: + nonlocal token + token = auth_token + return {} + + app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], MockCrud, Adapter) + + client = TestClient(app) + response = client.get('/ocpi/2.2.1/details', headers={ + 'authorization': 'Token Zm9v' + }) + + assert response.status_code == 200 + assert response.json()['data']['version'] == '2.2.1' + assert token == 'foo' + + +def test_get_versions_v_2_2_1_requires_auth(): app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) client = TestClient(app) response = client.get('/ocpi/2.2.1/details') - assert response.status_code == 200 - assert response.json()['data'][0]['version'] == '2.2.1' + assert response.status_code == 401