From 4a8e879d97b2aa6a5c9e326722f1ac5035779a5c Mon Sep 17 00:00:00 2001 From: VictorTechs <158027394+VictorTechs@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:46:21 +0100 Subject: [PATCH 1/2] GAIAG-17: backport ocpi module changes (#62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: backport extrawest repo * fix: fix commit (#63) * style: fix * style: fix * fix: test, styles and security * security: fix --------- Co-authored-by: SergeiVorobev <45231522+SergeiVorobev@users.noreply.github.com> --- .prospector.yaml | 4 + poetry.lock | 904 ++++++------------ py_ocpi/core/adapter.py | 326 ++++++- py_ocpi/core/authentication/__init__.py | 0 py_ocpi/core/authentication/authenticator.py | 53 + py_ocpi/core/authentication/verifier.py | 255 +++++ py_ocpi/core/config.py | 27 +- py_ocpi/core/crud.py | 239 +++-- py_ocpi/core/data_types.py | 97 +- py_ocpi/core/dependencies.py | 24 +- py_ocpi/core/endpoints.py | 108 --- py_ocpi/core/endpoints/__init__.py | 9 + py_ocpi/core/endpoints/utils.py | 56 ++ py_ocpi/core/endpoints/v_2_1_1/__init__.py | 9 + py_ocpi/core/endpoints/v_2_1_1/cpo.py | 26 + py_ocpi/core/endpoints/v_2_1_1/emsp.py | 29 + py_ocpi/core/endpoints/v_2_1_1/utils.py | 53 + py_ocpi/core/endpoints/v_2_2_1/__init__.py | 9 + py_ocpi/core/endpoints/v_2_2_1/cpo.py | 56 ++ py_ocpi/core/endpoints/v_2_2_1/emsp.py | 61 ++ py_ocpi/core/endpoints/v_2_2_1/utils.py | 54 ++ py_ocpi/core/enums.py | 59 +- py_ocpi/core/exceptions.py | 4 +- py_ocpi/core/logs.py | 52 + py_ocpi/core/push.py | 195 +++- py_ocpi/core/routers/__init__.py | 10 + py_ocpi/core/routers/v_2_1_1/__init__.py | 8 + py_ocpi/core/routers/v_2_2_1/__init__.py | 8 + py_ocpi/core/status.py | 61 +- py_ocpi/core/utils.py | 83 +- py_ocpi/main.py | 158 ++- py_ocpi/modules/__init__.py | 3 + py_ocpi/modules/cdrs/v_2_1_1/api/__init__.py | 2 + py_ocpi/modules/cdrs/v_2_1_1/api/cpo.py | 65 ++ py_ocpi/modules/cdrs/v_2_1_1/api/emsp.py | 105 ++ py_ocpi/modules/cdrs/v_2_1_1/enums.py | 31 + py_ocpi/modules/cdrs/v_2_1_1/schemas.py | 52 + py_ocpi/modules/cdrs/v_2_2_1/api/cpo.py | 47 +- py_ocpi/modules/cdrs/v_2_2_1/api/emsp.py | 86 +- py_ocpi/modules/cdrs/v_2_2_1/enums.py | 68 +- py_ocpi/modules/cdrs/v_2_2_1/schemas.py | 6 +- py_ocpi/modules/chargingprofiles/__init__.py | 0 .../chargingprofiles/v_2_2_1/api/__init__.py | 2 + .../chargingprofiles/v_2_2_1/api/cpo.py | 309 ++++++ .../chargingprofiles/v_2_2_1/api/emsp.py | 106 ++ .../v_2_2_1/background_tasks.py | 208 ++++ .../modules/chargingprofiles/v_2_2_1/enums.py | 29 + .../chargingprofiles/v_2_2_1/schemas.py | 82 ++ .../modules/commands/v_2_1_1/api/__init__.py | 2 + py_ocpi/modules/commands/v_2_1_1/api/cpo.py | 221 +++++ py_ocpi/modules/commands/v_2_1_1/api/emsp.py | 61 ++ py_ocpi/modules/commands/v_2_1_1/enums.py | 38 + py_ocpi/modules/commands/v_2_1_1/schemas.py | 58 ++ py_ocpi/modules/commands/v_2_2_1/api/cpo.py | 187 +++- py_ocpi/modules/commands/v_2_2_1/api/emsp.py | 44 +- py_ocpi/modules/commands/v_2_2_1/enums.py | 57 +- py_ocpi/modules/commands/v_2_2_1/schemas.py | 5 +- .../credentials/v_2_1_1/api/__init__.py | 2 + .../modules/credentials/v_2_1_1/api/cpo.py | 352 +++++++ .../modules/credentials/v_2_1_1/api/emsp.py | 351 +++++++ py_ocpi/modules/credentials/v_2_1_1/enums.py | 0 .../modules/credentials/v_2_1_1/schemas.py | 16 + .../modules/credentials/v_2_2_1/api/cpo.py | 315 ++++-- .../modules/credentials/v_2_2_1/api/emsp.py | 314 ++++-- py_ocpi/modules/hubclientinfo/__init__.py | 0 .../hubclientinfo/v_2_2_1/api/__init__.py | 2 + .../modules/hubclientinfo/v_2_2_1/api/cpo.py | 141 +++ .../modules/hubclientinfo/v_2_2_1/api/emsp.py | 141 +++ .../modules/hubclientinfo/v_2_2_1/enums.py | 11 + .../modules/hubclientinfo/v_2_2_1/schemas.py | 16 + py_ocpi/modules/locations/enums.py | 121 +++ py_ocpi/modules/locations/schemas.py | 101 ++ .../modules/locations/v_2_1_1/api/__init__.py | 2 + py_ocpi/modules/locations/v_2_1_1/api/cpo.py | 221 +++++ py_ocpi/modules/locations/v_2_1_1/api/emsp.py | 716 ++++++++++++++ py_ocpi/modules/locations/v_2_1_1/enums.py | 151 +++ py_ocpi/modules/locations/v_2_1_1/schemas.py | 163 ++++ py_ocpi/modules/locations/v_2_2_1/api/cpo.py | 218 ++++- py_ocpi/modules/locations/v_2_2_1/api/emsp.py | 846 ++++++++++++---- py_ocpi/modules/locations/v_2_2_1/enums.py | 303 ++---- py_ocpi/modules/locations/v_2_2_1/schemas.py | 104 +- .../modules/sessions/v_2_1_1/api/__init__.py | 2 + py_ocpi/modules/sessions/v_2_1_1/api/cpo.py | 66 ++ py_ocpi/modules/sessions/v_2_1_1/api/emsp.py | 222 +++++ py_ocpi/modules/sessions/v_2_1_1/enums.py | 24 + py_ocpi/modules/sessions/v_2_1_1/schemas.py | 44 + py_ocpi/modules/sessions/v_2_2_1/api/cpo.py | 83 +- py_ocpi/modules/sessions/v_2_2_1/api/emsp.py | 220 ++++- py_ocpi/modules/sessions/v_2_2_1/enums.py | 58 +- py_ocpi/modules/tariffs/enums.py | 14 + .../modules/tariffs/v_2_1_1/api/__init__.py | 2 + py_ocpi/modules/tariffs/v_2_1_1/api/cpo.py | 66 ++ py_ocpi/modules/tariffs/v_2_1_1/api/emsp.py | 277 ++++++ py_ocpi/modules/tariffs/v_2_1_1/enums.py | 16 + py_ocpi/modules/tariffs/v_2_1_1/schemas.py | 71 ++ py_ocpi/modules/tariffs/v_2_2_1/api/cpo.py | 46 +- py_ocpi/modules/tariffs/v_2_2_1/api/emsp.py | 193 +++- py_ocpi/modules/tariffs/v_2_2_1/enums.py | 64 +- py_ocpi/modules/tariffs/v_2_2_1/schemas.py | 17 +- .../modules/tokens/v_2_1_1/api/__init__.py | 2 + py_ocpi/modules/tokens/v_2_1_1/api/cpo.py | 218 +++++ py_ocpi/modules/tokens/v_2_1_1/api/emsp.py | 152 +++ py_ocpi/modules/tokens/v_2_1_1/enums.py | 49 + py_ocpi/modules/tokens/v_2_1_1/schemas.py | 57 ++ py_ocpi/modules/tokens/v_2_2_1/api/cpo.py | 229 ++++- py_ocpi/modules/tokens/v_2_2_1/api/emsp.py | 134 ++- py_ocpi/modules/tokens/v_2_2_1/enums.py | 29 +- py_ocpi/modules/tokens/v_2_2_1/schemas.py | 6 +- py_ocpi/modules/versions/api/__init__.py | 2 - py_ocpi/modules/versions/api/main.py | 15 - py_ocpi/modules/versions/api/v_2_2_1.py | 30 - py_ocpi/modules/versions/enums.py | 26 +- py_ocpi/modules/versions/main.py | 53 + py_ocpi/modules/versions/schemas.py | 22 +- .../modules/versions/v_2_1_1/api/__init__.py | 1 + py_ocpi/modules/versions/v_2_1_1/api/main.py | 55 ++ py_ocpi/modules/versions/v_2_1_1/enums.py | 1 + py_ocpi/modules/versions/v_2_1_1/schemas.py | 25 + .../modules/versions/v_2_2_1/api/__init__.py | 1 + py_ocpi/modules/versions/v_2_2_1/api/main.py | 55 ++ py_ocpi/modules/versions/v_2_2_1/enums.py | 16 + py_ocpi/modules/versions/v_2_2_1/schemas.py | 26 + py_ocpi/routers/__init__.py | 2 + py_ocpi/routers/v_2_1_1/cpo.py | 33 + py_ocpi/routers/v_2_1_1/emsp.py | 33 + py_ocpi/routers/v_2_2_1/cpo.py | 58 +- py_ocpi/routers/v_2_2_1/emsp.py | 58 +- tests/test_dependency_injection.py | 46 +- tests/test_main.py | 13 +- tests/test_modules/__init__.py | 0 tests/test_modules/mocks/async_client.py | 24 +- tests/test_modules/test_cdrs.py | 122 --- tests/test_modules/test_commands.py | 175 ---- tests/test_modules/test_credentials.py | 124 --- tests/test_modules/test_locations.py | 381 -------- tests/test_modules/test_sessions.py | 144 --- tests/test_modules/test_tariffs.py | 101 -- tests/test_modules/test_tokens.py | 173 ---- tests/test_modules/test_v_2_1_1/__init__.py | 0 .../test_v_2_1_1/test_cdrs/__init__.py | 0 .../test_v_2_1_1/test_cdrs/conftest.py | 41 + .../test_v_2_1_1/test_cdrs/test_cpo.py | 17 + .../test_v_2_1_1/test_cdrs/test_emsp.py | 42 + .../test_v_2_1_1/test_cdrs/utils.py | 85 ++ .../test_v_2_1_1/test_commands/__init__.py | 0 .../test_v_2_1_1/test_commands/conftest.py | 41 + .../test_v_2_1_1/test_commands/test_cpo.py | 152 +++ .../test_v_2_1_1/test_commands/test_emsp.py | 28 + .../test_v_2_1_1/test_commands/utils.py | 53 + .../test_v_2_1_1/test_credentials/__init__.py | 0 .../test_credentials/test_credentials.py | 95 ++ .../test_v_2_1_1/test_credentials/utils.py | 70 ++ .../test_v_2_1_1/test_locations/__init__.py | 0 .../test_v_2_1_1/test_locations/conftest.py | 41 + .../test_v_2_1_1/test_locations/test_cpo.py | 63 ++ .../test_v_2_1_1/test_locations/test_emsp.py | 174 ++++ .../test_v_2_1_1/test_locations/utils.py | 214 +++++ .../test_v_2_1_1/test_sessions/__init__.py | 0 .../test_v_2_1_1/test_sessions/conftest.py | 41 + .../test_v_2_1_1/test_sessions/test_cpo.py | 20 + .../test_v_2_1_1/test_sessions/test_emsp.py | 67 ++ .../test_v_2_1_1/test_sessions/utils.py | 85 ++ .../test_v_2_1_1/test_tariffs/__init__.py | 0 .../test_v_2_1_1/test_tariffs/conftest.py | 41 + .../test_v_2_1_1/test_tariffs/test_cpo.py | 20 + .../test_v_2_1_1/test_tariffs/test_emsp.py | 81 ++ .../test_v_2_1_1/test_tariffs/utils.py | 81 ++ .../test_v_2_1_1/test_tokens/__init__.py | 0 .../test_v_2_1_1/test_tokens/conftest.py | 41 + .../test_v_2_1_1/test_tokens/test_cpo.py | 70 ++ .../test_v_2_1_1/test_tokens/test_emsp.py | 39 + .../test_v_2_1_1/test_tokens/utils.py | 100 ++ .../test_v_2_1_1/test_versions/__init__.py | 0 .../test_v_2_1_1/test_versions/conftest.py | 0 .../test_versions/test_versions.py | 106 ++ .../test_v_2_1_1/test_versions/utils.py | 8 + tests/test_modules/test_v_2_2_1/__init__.py | 0 .../test_v_2_2_1/test_cdrs/__init__.py | 0 .../test_v_2_2_1/test_cdrs/conftest.py | 41 + .../test_v_2_2_1/test_cdrs/test_cpo.py | 17 + .../test_v_2_2_1/test_cdrs/test_emsp.py | 42 + .../test_v_2_2_1/test_cdrs/utils.py | 98 ++ .../test_chargingprofiles/__init__.py | 0 .../test_chargingprofiles/conftest.py | 42 + .../test_chargingprofiles/test_cpo.py | 205 ++++ .../test_chargingprofiles/test_emsp.py | 49 + .../test_chargingprofiles/utils.py | 83 ++ .../test_v_2_2_1/test_commands/__init__.py | 0 .../test_v_2_2_1/test_commands/conftest.py | 41 + .../test_v_2_2_1/test_commands/test_cpo.py | 183 ++++ .../test_v_2_2_1/test_commands/test_emsp.py | 28 + .../test_v_2_2_1/test_commands/utils.py | 55 ++ .../test_v_2_2_1/test_credentials/__init__.py | 0 .../test_credentials/test_credentials.py | 104 ++ .../test_v_2_2_1/test_credentials/utils.py | 80 ++ .../test_hubclientinfo/__init__.py | 0 .../test_hubclientinfo/conftest.py | 43 + .../test_hubclientinfo/test_cpo.py | 43 + .../test_hubclientinfo/test_emsp.py | 43 + .../test_v_2_2_1/test_hubclientinfo/utils.py | 58 ++ .../test_v_2_2_1/test_locations/__init__.py | 0 .../test_v_2_2_1/test_locations/conftest.py | 41 + .../test_v_2_2_1/test_locations/test_cpo.py | 63 ++ .../test_v_2_2_1/test_locations/test_emsp.py | 174 ++++ .../test_v_2_2_1/test_locations/utils.py | 229 +++++ .../test_v_2_2_1/test_sessions/__init__.py | 0 .../test_v_2_2_1/test_sessions/conftest.py | 41 + .../test_v_2_2_1/test_sessions/test_cpo.py | 52 + .../test_v_2_2_1/test_sessions/test_emsp.py | 70 ++ .../test_v_2_2_1/test_sessions/utils.py | 97 ++ .../test_v_2_2_1/test_tariffs/__init__.py | 0 .../test_v_2_2_1/test_tariffs/conftest.py | 41 + .../test_v_2_2_1/test_tariffs/test_cpo.py | 20 + .../test_v_2_2_1/test_tariffs/test_emsp.py | 57 ++ .../test_v_2_2_1/test_tariffs/utils.py | 79 ++ .../test_v_2_2_1/test_tokens/__init__.py | 0 .../test_v_2_2_1/test_tokens/conftest.py | 41 + .../test_v_2_2_1/test_tokens/test_cpo.py | 79 ++ .../test_v_2_2_1/test_tokens/test_emsp.py | 39 + .../test_v_2_2_1/test_tokens/utils.py | 106 ++ .../test_v_2_2_1/test_versions/__init__.py | 0 .../test_v_2_2_1/test_versions/conftest.py | 0 .../test_v_2_2_1/test_versions/test_utils.py | 8 + .../test_versions/test_versions.py | 106 ++ tests/test_modules/test_versions.py | 52 - tests/test_modules/utils.py | 27 + tests/test_pagination.py | 54 +- tests/test_push.py | 306 +++--- 228 files changed, 14990 insertions(+), 3657 deletions(-) create mode 100644 py_ocpi/core/authentication/__init__.py create mode 100644 py_ocpi/core/authentication/authenticator.py create mode 100644 py_ocpi/core/authentication/verifier.py delete mode 100644 py_ocpi/core/endpoints.py create mode 100644 py_ocpi/core/endpoints/__init__.py create mode 100644 py_ocpi/core/endpoints/utils.py create mode 100644 py_ocpi/core/endpoints/v_2_1_1/__init__.py create mode 100644 py_ocpi/core/endpoints/v_2_1_1/cpo.py create mode 100644 py_ocpi/core/endpoints/v_2_1_1/emsp.py create mode 100644 py_ocpi/core/endpoints/v_2_1_1/utils.py create mode 100644 py_ocpi/core/endpoints/v_2_2_1/__init__.py create mode 100644 py_ocpi/core/endpoints/v_2_2_1/cpo.py create mode 100644 py_ocpi/core/endpoints/v_2_2_1/emsp.py create mode 100644 py_ocpi/core/endpoints/v_2_2_1/utils.py create mode 100644 py_ocpi/core/logs.py create mode 100644 py_ocpi/core/routers/__init__.py create mode 100644 py_ocpi/core/routers/v_2_1_1/__init__.py create mode 100644 py_ocpi/core/routers/v_2_2_1/__init__.py create mode 100644 py_ocpi/modules/cdrs/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/cdrs/v_2_1_1/api/cpo.py create mode 100644 py_ocpi/modules/cdrs/v_2_1_1/api/emsp.py create mode 100644 py_ocpi/modules/cdrs/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/cdrs/v_2_1_1/schemas.py create mode 100644 py_ocpi/modules/chargingprofiles/__init__.py create mode 100644 py_ocpi/modules/chargingprofiles/v_2_2_1/api/__init__.py create mode 100644 py_ocpi/modules/chargingprofiles/v_2_2_1/api/cpo.py create mode 100644 py_ocpi/modules/chargingprofiles/v_2_2_1/api/emsp.py create mode 100644 py_ocpi/modules/chargingprofiles/v_2_2_1/background_tasks.py create mode 100644 py_ocpi/modules/chargingprofiles/v_2_2_1/enums.py create mode 100644 py_ocpi/modules/chargingprofiles/v_2_2_1/schemas.py create mode 100644 py_ocpi/modules/commands/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/commands/v_2_1_1/api/cpo.py create mode 100644 py_ocpi/modules/commands/v_2_1_1/api/emsp.py create mode 100644 py_ocpi/modules/commands/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/commands/v_2_1_1/schemas.py create mode 100644 py_ocpi/modules/credentials/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/credentials/v_2_1_1/api/cpo.py create mode 100644 py_ocpi/modules/credentials/v_2_1_1/api/emsp.py create mode 100644 py_ocpi/modules/credentials/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/credentials/v_2_1_1/schemas.py create mode 100644 py_ocpi/modules/hubclientinfo/__init__.py create mode 100644 py_ocpi/modules/hubclientinfo/v_2_2_1/api/__init__.py create mode 100644 py_ocpi/modules/hubclientinfo/v_2_2_1/api/cpo.py create mode 100644 py_ocpi/modules/hubclientinfo/v_2_2_1/api/emsp.py create mode 100644 py_ocpi/modules/hubclientinfo/v_2_2_1/enums.py create mode 100644 py_ocpi/modules/hubclientinfo/v_2_2_1/schemas.py create mode 100644 py_ocpi/modules/locations/enums.py create mode 100644 py_ocpi/modules/locations/schemas.py create mode 100644 py_ocpi/modules/locations/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/locations/v_2_1_1/api/cpo.py create mode 100644 py_ocpi/modules/locations/v_2_1_1/api/emsp.py create mode 100644 py_ocpi/modules/locations/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/locations/v_2_1_1/schemas.py create mode 100644 py_ocpi/modules/sessions/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/sessions/v_2_1_1/api/cpo.py create mode 100644 py_ocpi/modules/sessions/v_2_1_1/api/emsp.py create mode 100644 py_ocpi/modules/sessions/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/sessions/v_2_1_1/schemas.py create mode 100644 py_ocpi/modules/tariffs/enums.py create mode 100644 py_ocpi/modules/tariffs/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/tariffs/v_2_1_1/api/cpo.py create mode 100644 py_ocpi/modules/tariffs/v_2_1_1/api/emsp.py create mode 100644 py_ocpi/modules/tariffs/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/tariffs/v_2_1_1/schemas.py create mode 100644 py_ocpi/modules/tokens/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/tokens/v_2_1_1/api/cpo.py create mode 100644 py_ocpi/modules/tokens/v_2_1_1/api/emsp.py create mode 100644 py_ocpi/modules/tokens/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/tokens/v_2_1_1/schemas.py delete mode 100644 py_ocpi/modules/versions/api/__init__.py delete mode 100644 py_ocpi/modules/versions/api/main.py delete mode 100644 py_ocpi/modules/versions/api/v_2_2_1.py create mode 100644 py_ocpi/modules/versions/main.py create mode 100644 py_ocpi/modules/versions/v_2_1_1/api/__init__.py create mode 100644 py_ocpi/modules/versions/v_2_1_1/api/main.py create mode 100644 py_ocpi/modules/versions/v_2_1_1/enums.py create mode 100644 py_ocpi/modules/versions/v_2_1_1/schemas.py create mode 100644 py_ocpi/modules/versions/v_2_2_1/api/__init__.py create mode 100644 py_ocpi/modules/versions/v_2_2_1/api/main.py create mode 100644 py_ocpi/modules/versions/v_2_2_1/enums.py create mode 100644 py_ocpi/modules/versions/v_2_2_1/schemas.py create mode 100644 py_ocpi/routers/v_2_1_1/cpo.py create mode 100644 py_ocpi/routers/v_2_1_1/emsp.py create mode 100644 tests/test_modules/__init__.py delete mode 100644 tests/test_modules/test_cdrs.py delete mode 100644 tests/test_modules/test_commands.py delete mode 100644 tests/test_modules/test_credentials.py delete mode 100644 tests/test_modules/test_locations.py delete mode 100644 tests/test_modules/test_sessions.py delete mode 100644 tests/test_modules/test_tariffs.py delete mode 100644 tests/test_modules/test_tokens.py create mode 100644 tests/test_modules/test_v_2_1_1/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_cdrs/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_cdrs/conftest.py create mode 100644 tests/test_modules/test_v_2_1_1/test_cdrs/test_cpo.py create mode 100644 tests/test_modules/test_v_2_1_1/test_cdrs/test_emsp.py create mode 100644 tests/test_modules/test_v_2_1_1/test_cdrs/utils.py create mode 100644 tests/test_modules/test_v_2_1_1/test_commands/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_commands/conftest.py create mode 100644 tests/test_modules/test_v_2_1_1/test_commands/test_cpo.py create mode 100644 tests/test_modules/test_v_2_1_1/test_commands/test_emsp.py create mode 100644 tests/test_modules/test_v_2_1_1/test_commands/utils.py create mode 100644 tests/test_modules/test_v_2_1_1/test_credentials/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_credentials/test_credentials.py create mode 100644 tests/test_modules/test_v_2_1_1/test_credentials/utils.py create mode 100644 tests/test_modules/test_v_2_1_1/test_locations/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_locations/conftest.py create mode 100644 tests/test_modules/test_v_2_1_1/test_locations/test_cpo.py create mode 100644 tests/test_modules/test_v_2_1_1/test_locations/test_emsp.py create mode 100644 tests/test_modules/test_v_2_1_1/test_locations/utils.py create mode 100644 tests/test_modules/test_v_2_1_1/test_sessions/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_sessions/conftest.py create mode 100644 tests/test_modules/test_v_2_1_1/test_sessions/test_cpo.py create mode 100644 tests/test_modules/test_v_2_1_1/test_sessions/test_emsp.py create mode 100644 tests/test_modules/test_v_2_1_1/test_sessions/utils.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tariffs/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tariffs/conftest.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tariffs/test_cpo.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tariffs/test_emsp.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tariffs/utils.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tokens/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tokens/conftest.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tokens/test_cpo.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tokens/test_emsp.py create mode 100644 tests/test_modules/test_v_2_1_1/test_tokens/utils.py create mode 100644 tests/test_modules/test_v_2_1_1/test_versions/__init__.py create mode 100644 tests/test_modules/test_v_2_1_1/test_versions/conftest.py create mode 100644 tests/test_modules/test_v_2_1_1/test_versions/test_versions.py create mode 100644 tests/test_modules/test_v_2_1_1/test_versions/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_cdrs/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_cdrs/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_cdrs/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_cdrs/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_cdrs/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_chargingprofiles/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_chargingprofiles/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_chargingprofiles/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_commands/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_commands/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_commands/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_commands/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_commands/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_credentials/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_credentials/test_credentials.py create mode 100644 tests/test_modules/test_v_2_2_1/test_credentials/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_hubclientinfo/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_hubclientinfo/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_hubclientinfo/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_locations/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_locations/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_locations/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_locations/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_locations/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_sessions/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_sessions/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_sessions/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_sessions/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_sessions/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tariffs/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tariffs/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tariffs/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tariffs/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tariffs/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tokens/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tokens/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tokens/test_cpo.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tokens/test_emsp.py create mode 100644 tests/test_modules/test_v_2_2_1/test_tokens/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_versions/__init__.py create mode 100644 tests/test_modules/test_v_2_2_1/test_versions/conftest.py create mode 100644 tests/test_modules/test_v_2_2_1/test_versions/test_utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_versions/test_versions.py delete mode 100644 tests/test_modules/test_versions.py create mode 100644 tests/test_modules/utils.py diff --git a/.prospector.yaml b/.prospector.yaml index 970d3d7..227251a 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -43,9 +43,13 @@ pylint: - assignment-from-none - redefined-outer-name - no-self-argument + - unnecessary-pass + - logging-fstring-interpolation + - import-self options: max-locals: 25 max-line-length: 120 + max-positional-arguments: 10 pycodestyle: disable: diff --git a/poetry.lock b/poetry.lock index 474480e..7ffe2d7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,72 +1,57 @@ [[package]] name = "anyio" -version = "3.6.1" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.9" [package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +doc = ["packaging", "Sphinx (>=7.4,<8.0)", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)", "truststore (>=0.9.1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "astroid" -version = "2.12.10" +version = "3.3.5" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.9.0" [package.dependencies] -lazy-object-proxy = ">=1.4.0" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, -] - -[[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "bandit" -version = "1.7.4" +version = "1.7.10" description = "Security oriented static analyser for python code." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -GitPython = ">=1.0.1" PyYAML = ">=5.3.1" +rich = "*" stevedore = ">=1.20.0" [package.extras] -test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)"] -toml = ["toml"] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["sarif-om (>=1.0.4)", "jschema-to-python (>=1.2.3)"] +test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)"] +toml = ["tomli (>=1.1.0)"] yaml = ["pyyaml"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -74,44 +59,42 @@ python-versions = ">=3.6" [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] +python-versions = ">=3.7.0" [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" -version = "6.4.4" +version = "7.6.4" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" [package.extras] toml = ["tomli"] [[package]] name = "dill" -version = "0.3.5.1" -description = "serialize all of python" +version = "0.3.9" +description = "serialize all of Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +python-versions = ">=3.8" [package.extras] graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "dodgy" @@ -121,6 +104,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "fastapi" version = "0.68.2" @@ -141,16 +135,16 @@ test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "pytest-async [[package]] name = "flake8" -version = "2.3.0" -description = "the modular source code checker: pep8, pyflakes and co" +version = "7.1.1" +description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.8.1" [package.dependencies] -mccabe = ">=0.2.1" -pep8 = ">=1.5.7" -pyflakes = ">=0.8.1" +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "flake8-polyfill" @@ -165,19 +159,19 @@ flake8 = "*" [[package]] name = "gitdb" -version = "4.0.9" +version = "4.0.11" description = "Git Object Database" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.27" -description = "GitPython is a python library used to interact with Git repositories" +version = "3.1.43" +description = "GitPython is a Python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" @@ -185,26 +179,30 @@ python-versions = ">=3.7" [package.dependencies] gitdb = ">=4.0.1,<5" +[package.extras] +doc = ["sphinx (==4.3.2)", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinx-autodoc-typehints"] +test = ["coverage", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions", "mock"] + [[package]] name = "h11" -version = "0.12.0" +version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "httpcore" -version = "0.15.0" +version = "0.16.3" description = "A minimal low-level HTTP client." category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -anyio = ">=3.0.0,<4.0.0" +anyio = ">=3.0,<5.0" certifi = "*" -h11 = ">=0.11,<0.13" +h11 = ">=0.13,<0.15" sniffio = ">=1.0.0,<2.0.0" [package.extras] @@ -213,7 +211,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" -version = "0.23.0" +version = "0.23.3" description = "The next generation HTTP client." category = "main" optional = false @@ -221,89 +219,99 @@ python-versions = ">=3.7" [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.16.0" +httpcore = ">=0.15.0,<0.17.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotlicffi", "brotli"] -cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "idna" -version = "3.4" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" + +[package.extras] +all = ["ruff (>=0.6.2)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "flake8 (>=7.1.1)"] [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "isort" -version = "5.10.1" +version = "5.13.2" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.8.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] -plugins = ["setuptools"] +colors = ["colorama (>=0.4.6)"] [[package]] -name = "lazy-object-proxy" -version = "1.7.1" -description = "A fast and thorough lazy object proxy." +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code_style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx-book-theme", "jupyter-sphinx"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "mccabe" -version = "0.6.1" +version = "0.7.0" description = "McCabe checker, plugin for flake8" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "dev" +optional = false +python-versions = ">=3.7" [[package]] name = "packaging" -version = "21.3" +version = "24.1" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.8" [[package]] name = "pbr" -version = "5.10.0" +version = "6.1.0" description = "Python Build Reasonableness" category = "dev" optional = false python-versions = ">=2.6" -[[package]] -name = "pep8" -version = "1.7.1" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "pep8-naming" version = "0.10.0" @@ -317,23 +325,24 @@ flake8-polyfill = ">=1.0.2,<2" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx-autodoc-typehints (>=2.4)", "sphinx (>=8.0.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest (>=8.3.2)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [package.extras] dev = ["pre-commit", "tox"] @@ -341,63 +350,57 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prospector" -version = "1.7.7" -description = "" +version = "1.12.1" +description = "Prospector is a tool to analyse Python code by aggregating the result of other tools." category = "dev" optional = false -python-versions = ">=3.6.2,<4.0" +python-versions = "<4.0,>=3.9" [package.dependencies] dodgy = ">=0.2.1,<0.3.0" -mccabe = ">=0.6.0,<0.7.0" +flake8 = "*" +GitPython = ">=3.1.27,<4.0.0" +mccabe = ">=0.7.0,<0.8.0" +packaging = "*" pep8-naming = ">=0.3.3,<=0.10.0" -pycodestyle = ">=2.6.0,<2.9.0" +pycodestyle = ">=2.9.0" pydocstyle = ">=2.0.0" -pyflakes = ">=2.2.0,<3" -pylint = ">=2.8.3" +pyflakes = ">=2.2.0" +pylint = ">=3.0" pylint-celery = "0.3" -pylint-django = ">=2.5,<2.6" +pylint-django = ">=2.6.1" pylint-flask = "0.6" -pylint-plugin-utils = ">=0.7,<0.8" PyYAML = "*" -requirements-detector = ">=0.7,<0.8" +requirements-detector = ">=1.3.1" setoptconf-tmp = ">=0.3.1,<0.4.0" toml = ">=0.10.2,<0.11.0" [package.extras] -with_bandit = ["bandit (>=1.5.1)"] -with_everything = ["bandit (>=1.5.1)", "frosted (>=1.4.1)", "mypy (>=0.600)", "pyroma (>=2.4)", "vulture (>=1.5)"] -with_frosted = ["frosted (>=1.4.1)"] -with_mypy = ["mypy (>=0.600)"] -with_pyroma = ["pyroma (>=2.4)"] -with_vulture = ["vulture (>=1.5)"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +with-bandit = ["bandit (>=1.5.1)"] +with_everything = ["bandit (>=1.5.1)", "mypy (>=0.600)", "pyright (>=1.1.3)", "pyroma (>=2.4)", "vulture (>=1.5)"] +with-mypy = ["mypy (>=0.600)"] +with-pyright = ["pyright (>=1.1.3)"] +with-pyroma = ["pyroma (>=2.4)"] +with-vulture = ["vulture (>=1.5)"] [[package]] name = "pycodestyle" -version = "2.8.0" +version = "2.12.1" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.8" [[package]] name = "pydantic" -version = "1.10.2" +version = "1.10.18" description = "Data validation and settings management using python type hints" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.2.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] @@ -405,39 +408,54 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pydocstyle" -version = "6.1.1" +version = "6.3.0" description = "Python docstring style checker" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -snowballstemmer = "*" +snowballstemmer = ">=2.2.0" [package.extras] -toml = ["toml"] +toml = ["tomli (>=1.2.3)"] [[package]] name = "pyflakes" -version = "2.5.0" +version = "3.2.0" description = "passive checker of Python programs" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "2.15.3" +version = "3.3.1" description = "python code static checker" category = "dev" optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.9.0" [package.dependencies] -astroid = ">=2.12.10,<=2.14.0-dev0" +astroid = ">=3.3.4,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = ">=0.2" -isort = ">=4.2.5,<6" +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, +] +isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -463,19 +481,18 @@ pylint-plugin-utils = ">=0.2.1" [[package]] name = "pylint-django" -version = "2.5.3" +version = "2.6.1" description = "A Pylint plugin to help Pylint understand the Django web framework" category = "dev" optional = false -python-versions = "*" +python-versions = "<4.0,>=3.9" [package.dependencies] -pylint = ">=2.0,<3" -pylint-plugin-utils = ">=0.7" +pylint = ">=3.0,<4" +pylint-plugin-utils = ">=0.8" [package.extras] -for_tests = ["django-tables2", "factory-boy", "coverage", "pytest", "wheel", "django-tastypie", "pylint (>=2.13)"] -with_django = ["django"] +with-django = ["Django (>=2.2)"] [[package]] name = "pylint-flask" @@ -490,45 +507,33 @@ pylint-plugin-utils = ">=0.2.1" [[package]] name = "pylint-plugin-utils" -version = "0.7" +version = "0.8.2" description = "Utilities and helpers for writing Pylint plugins" category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7,<4.0" [package.dependencies] pylint = ">=1.7" -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] - [[package]] name = "pytest" -version = "7.1.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" @@ -563,40 +568,43 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.2" description = "YAML parser and emitter for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [[package]] name = "requests" -version = "2.28.1" +version = "2.32.3" description = "Python HTTP for Humans." category = "dev" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.8" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requirements-detector" -version = "0.7" +version = "1.3.1" description = "Python tool to find and list requirements of a Python project" category = "dev" optional = false -python-versions = "*" +python-versions = "<4.0,>=3.8" [package.dependencies] -astroid = ">=1.4" +astroid = ">=3.0,<4.0" +packaging = ">=21.3" +semver = ">=3.0.0,<4.0.0" +toml = ">=0.10.2,<0.11.0" [[package]] name = "rfc3986" @@ -612,6 +620,30 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" +optional = false +python-versions = ">=3.8.0" + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "semver" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" +category = "dev" +optional = false +python-versions = ">=3.7" + [[package]] name = "setoptconf-tmp" version = "0.3.1" @@ -625,15 +657,15 @@ yaml = ["pyyaml"] [[package]] name = "smmap" -version = "5.0.0" +version = "5.0.1" description = "A pure Python implementation of a sliding window memory map manager" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" category = "main" optional = false @@ -660,14 +692,14 @@ full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "p [[package]] name = "stevedore" -version = "4.0.0" +version = "5.3.0" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false python-versions = ">=3.8" [package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" +pbr = ">=2.0.0" [[package]] name = "toml" @@ -679,48 +711,41 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [[package]] name = "tomlkit" -version = "0.11.5" +version = "0.13.2" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.8" [[package]] name = "typing-extensions" -version = "4.3.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [[package]] name = "urllib3" -version = "1.26.12" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=3.8" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "1.1" @@ -728,432 +753,63 @@ python-versions = "^3.9" content-hash = "efe95d4bdce74a1b92a7672bd5a1f442934ca36c517550d53725fa49cb2752fa" [metadata.files] -anyio = [ - {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, - {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, -] -astroid = [ - {file = "astroid-2.12.10-py3-none-any.whl", hash = "sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5"}, - {file = "astroid-2.12.10.tar.gz", hash = "sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581"}, -] -attrs = [] -bandit = [ - {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, - {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, -] -certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -coverage = [ - {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, - {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, - {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, - {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, - {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, - {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, - {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, - {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, - {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, - {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, - {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, - {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, - {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, - {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, - {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, - {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, -] -dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, -] -dodgy = [ - {file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"}, - {file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"}, -] +anyio = [] +astroid = [] +bandit = [] +certifi = [] +charset-normalizer = [] +colorama = [] +coverage = [] +dill = [] +dodgy = [] +exceptiongroup = [] fastapi = [] flake8 = [] -flake8-polyfill = [ - {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, - {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, -] -gitdb = [ - {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, - {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, -] -gitpython = [ - {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, - {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, -] -h11 = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, -] -httpcore = [ - {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, - {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, -] -httpx = [ - {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, - {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, - {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pbr = [ - {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"}, - {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, -] -pep8 = [] -pep8-naming = [ - {file = "pep8-naming-0.10.0.tar.gz", hash = "sha256:f3b4a5f9dd72b991bf7d8e2a341d2e1aa3a884a769b5aaac4f56825c1763bf3a"}, - {file = "pep8_naming-0.10.0-py2.py3-none-any.whl", hash = "sha256:5d9f1056cb9427ce344e98d1a7f5665710e2f20f748438e308995852cfa24164"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -prospector = [ - {file = "prospector-1.7.7-py3-none-any.whl", hash = "sha256:2dec5dac06f136880a3710996c0886dcc99e739007bbc05afc32884973f5c058"}, - {file = "prospector-1.7.7.tar.gz", hash = "sha256:c04b3d593e7c525cf9a742fed62afbe02e2874f0e42f2f56a49378fd94037360"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pydantic = [ - {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, - {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, - {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, - {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, - {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, - {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, - {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, - {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, - {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, - {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, - {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, - {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, - {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, - {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, - {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, - {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, - {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, - {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, - {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, - {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, - {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, - {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, - {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, - {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, - {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, - {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, - {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, - {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, - {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, - {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, - {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, - {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, - {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, - {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, - {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, - {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, -] -pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, -] +flake8-polyfill = [] +gitdb = [] +gitpython = [] +h11 = [] +httpcore = [] +httpx = [] +idna = [] +iniconfig = [] +isort = [] +markdown-it-py = [] +mccabe = [] +mdurl = [] +packaging = [] +pbr = [] +pep8-naming = [] +platformdirs = [] +pluggy = [] +prospector = [] +pycodestyle = [] +pydantic = [] +pydocstyle = [] pyflakes = [] -pylint = [ - {file = "pylint-2.15.3-py3-none-any.whl", hash = "sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1"}, - {file = "pylint-2.15.3.tar.gz", hash = "sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b"}, -] -pylint-celery = [ - {file = "pylint-celery-0.3.tar.gz", hash = "sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"}, -] -pylint-django = [ - {file = "pylint-django-2.5.3.tar.gz", hash = "sha256:0ac090d106c62fe33782a1d01bda1610b761bb1c9bf5035ced9d5f23a13d8591"}, - {file = "pylint_django-2.5.3-py3-none-any.whl", hash = "sha256:56b12b6adf56d548412445bd35483034394a1a94901c3f8571980a13882299d5"}, -] -pylint-flask = [ - {file = "pylint-flask-0.6.tar.gz", hash = "sha256:f4d97de2216bf7bfce07c9c08b166e978fe9f2725de2a50a9845a97de7e31517"}, -] -pylint-plugin-utils = [ - {file = "pylint-plugin-utils-0.7.tar.gz", hash = "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd"}, - {file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, -] -pytest-cov = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -requirements-detector = [ - {file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"}, -] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] -setoptconf-tmp = [ - {file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"}, - {file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"}, -] -smmap = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, -] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -starlette = [ - {file = "starlette-0.14.2-py3-none-any.whl", hash = "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed"}, - {file = "starlette-0.14.2.tar.gz", hash = "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa"}, -] +pygments = [] +pylint = [] +pylint-celery = [] +pylint-django = [] +pylint-flask = [] +pylint-plugin-utils = [] +pytest = [] +pytest-asyncio = [] +pytest-cov = [] +pyyaml = [] +requests = [] +requirements-detector = [] +rfc3986 = [] +rich = [] +semver = [] +setoptconf-tmp = [] +smmap = [] +sniffio = [] +snowballstemmer = [] +starlette = [] stevedore = [] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tomlkit = [ - {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, - {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, -] -typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, -] -urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, -] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] +toml = [] +tomli = [] +tomlkit = [] +typing-extensions = [] +urllib3 = [] diff --git a/py_ocpi/core/adapter.py b/py_ocpi/core/adapter.py index 51e5c1f..88881ab 100644 --- a/py_ocpi/core/adapter.py +++ b/py_ocpi/core/adapter.py @@ -1,124 +1,386 @@ +from abc import ABC, abstractmethod + +from py_ocpi.core.utils import get_module_model from py_ocpi.modules.versions.enums import VersionNumber -class Adapter: - @classmethod - def location_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): +class Adapter(ABC): + @abstractmethod + def location_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI Location schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: Location: The object data in proper OCPI schema """ + pass - @classmethod - def session_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def session_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI Session schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: Session: The object data in proper OCPI schema """ + pass - @classmethod - def charging_preference_adapter(cls, data: dict, - version: VersionNumber = VersionNumber.latest): + @abstractmethod + def charging_preference_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI ChargingPreference schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: ChargingPreference: The object data in proper OCPI schema """ + pass - @classmethod - def credentials_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def credentials_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI Credential schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: Credential: The object data in proper OCPI schema """ + pass - @classmethod - def cdr_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def cdr_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI CDR schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: CDR: The object data in proper OCPI schema """ + pass - @classmethod - def tariff_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def tariff_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI Tariff schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: Tariff: The object data in proper OCPI schema """ + pass - @classmethod - def command_response_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def command_response_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI CommandResponse schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: CommandResponse: The object data in proper OCPI schema """ + pass - @classmethod - def command_result_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def command_result_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI CommandResult schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: CommandResult: The object data in proper OCPI schema """ + pass - @classmethod - def token_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def token_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI Token schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: Token: The object data in proper OCPI schema """ + pass - @classmethod - def authorization_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): + @abstractmethod + def authorization_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): """Adapt the data to OCPI AuthorizationInfo schema Args: data (dict): The object details - version (VersionNumber, optional): The version number of the caller OCPI module + version (VersionNumber, optional): + The version number of the caller OCPI module Returns: AuthorizationInfo: The object data in proper OCPI schema """ + pass + + @abstractmethod + def hubclientinfo_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ClientInfo schema + + Args: + data (dict): The object details + version (VersionNumber, optional): + The version number of the caller OCPI module + + Returns: + ClientInfo: The object data in proper OCPI schema + """ + pass + + @abstractmethod + def charging_profile_response_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ChargingProfileResponse schema + + Args: + data (dict): The object details + version (VersionNumber, optional): + The version number of the caller OCPI module + + Returns: + ChargingProfileResponse: The object data in proper OCPI schema + """ + pass + + @abstractmethod + def active_charging_profile_result_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ActiveChargingProfileResult schema + + Args: + data (dict): The object details + version (VersionNumber, optional): + The version number of the caller OCPI module + + + ActiveChargingProfileResult: The object data in proper OCPI schema + """ + pass + + @abstractmethod + def clear_profile_result_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ClearProfileResult schema + + Args: + data (dict): The object details + version (VersionNumber, optional): + The version number of the caller OCPI module + + Returns: + ClearProfileResult: The object data in proper OCPI schema + """ + pass + + +class BaseAdapter(Adapter): + @classmethod + def location_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI Location schema""" + return get_module_model( + class_name="Location", + module_name="locations", + version_name=version.name, + )(**data) + + @classmethod + def session_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI Session schema""" + return get_module_model( + class_name="Session", + module_name="sessions", + version_name=version.name, + )(**data) + + @classmethod + def charging_preference_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ChargingPreference schema""" + return get_module_model( + class_name="ChargingPreferences", + module_name="sessions", + version_name=version.name, + )(**data) + + @classmethod + def credentials_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI Credentials schema""" + return get_module_model( + class_name="Credentials", + module_name="credentials", + version_name=version.name, + )(**data) + + @classmethod + def cdr_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI Cdr schema""" + return get_module_model( + class_name="Cdr", + module_name="cdrs", + version_name=version.name, + )(**data) + + @classmethod + def tariff_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI Tariff schema""" + return get_module_model( + class_name="Tariff", + module_name="tariffs", + version_name=version.name, + )(**data) + + @classmethod + def command_response_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI CommandResponse schema""" + return get_module_model( + class_name="CommandResponse", + module_name="commands", + version_name=version.name, + )(**data) + + @classmethod + def command_result_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI CommandResult schema""" + return get_module_model( + class_name="CommandResult", + module_name="commands", + version_name=version.name, + )(**data) + + @classmethod + def token_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI Token schema""" + return get_module_model( + class_name="Token", + module_name="tokens", + version_name=version.name, + )(**data) + + @classmethod + def authorization_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI AuthorizationInfo schema""" + return get_module_model( + class_name="AuthorizationInfo", + module_name="tokens", + version_name=version.name, + )(**data) + + @classmethod + def hubclientinfo_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ClientInfo schema""" + return get_module_model( + class_name="ClientInfo", + module_name="hubclientinfo", + version_name=version.name, + )(**data) + + @classmethod + def charging_profile_response_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ChargingProfileResponse schema""" + return get_module_model( + class_name="ChargingProfileResponse", + module_name="chargingprofiles", + version_name=version.name, + )(**data) + + @classmethod + def active_charging_profile_result_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ActiveChargingProfileResult schema""" + return get_module_model( + class_name="ActiveChargingProfileResult", + module_name="chargingprofiles", + version_name=version.name, + )(**data) + + @classmethod + def clear_profile_result_adapter( + cls, data: dict, version: VersionNumber = VersionNumber.latest + ): + """Adapt the data to OCPI ClearProfileResult schema""" + return get_module_model( + class_name="ClearProfileResult", + module_name="chargingprofiles", + version_name=version.name, + )(**data) diff --git a/py_ocpi/core/authentication/__init__.py b/py_ocpi/core/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py_ocpi/core/authentication/authenticator.py b/py_ocpi/core/authentication/authenticator.py new file mode 100644 index 0000000..422b86c --- /dev/null +++ b/py_ocpi/core/authentication/authenticator.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod + +from typing import List, Union + +from py_ocpi.core.exceptions import AuthorizationOCPIError +from py_ocpi.core.config import logger + + +class Authenticator(ABC): + """Base class responsible for verifying authorization tokens.""" + + @classmethod + async def authenticate(cls, auth_token: str) -> None: + """Authenticate given auth token. + + :raises AuthorizationOCPIError: If auth_token is not in a given + list of verified tokens C. + """ + list_token_c = await cls.get_valid_token_c() + if auth_token not in list_token_c: + logger.debug(f"Given `{auth_token}` token is not valid") + raise AuthorizationOCPIError + + @classmethod + async def authenticate_credentials( + cls, + auth_token: str, + ) -> Union[str, dict, None]: + """Authenticate given auth token where both tokens valid.""" + if auth_token: + list_token_a = await cls.get_valid_token_a() + if auth_token in list_token_a: + logger.debug(f"Token A `{auth_token}` is used.") + return {} + + list_token_c = await cls.get_valid_token_c() + if auth_token in list_token_c: + logger.debug(f"Token C `{auth_token}` is used.") + return auth_token + logger.debug(f"Token `{auth_token}` is not of type A or C.") + return None + + @classmethod + @abstractmethod + async def get_valid_token_c(cls) -> List[str]: + """Return valid token c list.""" + pass + + @classmethod + @abstractmethod + async def get_valid_token_a(cls) -> List[str]: + """Return valid token a list.""" + pass diff --git a/py_ocpi/core/authentication/verifier.py b/py_ocpi/core/authentication/verifier.py new file mode 100644 index 0000000..e9703aa --- /dev/null +++ b/py_ocpi/core/authentication/verifier.py @@ -0,0 +1,255 @@ +from typing import Union + +from fastapi import ( + Depends, + Header, + Path, + Security, + status, + Query, + HTTPException +) +from fastapi.security import APIKeyHeader + +from py_ocpi.core.authentication.authenticator import Authenticator +from py_ocpi.core.config import logger, settings +from py_ocpi.core.dependencies import get_authenticator +from py_ocpi.core.exceptions import AuthorizationOCPIError +from py_ocpi.core.utils import decode_string_base64 +from py_ocpi.modules.versions.enums import VersionNumber + +api_key_header = APIKeyHeader( + name="authorization", + description="API key with `Token ` prefix.", + scheme_name="Token", +) +auth_verifier = Security(api_key_header) if not settings.NO_AUTH else "" + + +class AuthorizationVerifier: + """ + A class responsible for verifying authorization tokens + based on the specified version number. + + :param version (VersionNumber): OCPI version used. + """ + + def __init__(self, version: VersionNumber) -> None: + self.version = version + + async def __call__( + self, + authorization: str = auth_verifier, + authenticator: Authenticator = Depends(get_authenticator), + ): + """ + Verifies the authorization token using the specified version + and an Authenticator. + + :param authorization (str): The authorization header containing + the token. + :param authenticator (Authenticator): An Authenticator instance used + for authentication. + + :raises AuthorizationOCPIError: If there is an issue with + the authorization token. + """ + if settings.NO_AUTH and authorization == "": + logger.debug("Authentication skipped due to NO_AUTH setting.") + return True + + try: + token = authorization.split()[1] + if self.version.startswith("2.2"): + try: + token = decode_string_base64(token) + except UnicodeDecodeError as exc: + logger.debug( + f"Token `{token}` cannot be decoded. " + "Check if the token is already encoded." + ) + raise AuthorizationOCPIError from exc + await authenticator.authenticate(token) + except IndexError as exc: + logger.debug( + "Token `%s` cannot be split in parts. " + "Check if it starts with `Token `" + ) + raise AuthorizationOCPIError from exc + + +class CredentialsAuthorizationVerifier: + """ + A class responsible for verifying authorization tokens + based on the specified version number. + + :param version (VersionNumber): OCPI version used. + """ + + def __init__(self, version: Union[VersionNumber, None]) -> None: + self.version = version + + async def __call__( + self, + authorization: str = Security(api_key_header), + authenticator: Authenticator = Depends(get_authenticator), + ) -> Union[str, dict, None]: + """ + Verifies the authorization token using the specified version + and an Authenticator. + + :param authorization (str): The authorization header containing + the token. + :param authenticator (Authenticator): An Authenticator instance used + for authentication. + + :raises AuthorizationOCPIError: If there is an issue with + the authorization token. + """ + try: + token = authorization.split()[1] + except IndexError as exc: + logger.debug("Token cannot be split in parts. Check if it starts with `Token `") + raise AuthorizationOCPIError from exc + + if self.version: + if self.version.startswith("2.2"): + try: + token = decode_string_base64(token) + except UnicodeDecodeError as exc: + logger.debug( + f"Token `{token}` cannot be decoded. " + "Check if the token is already encoded." + ) + raise AuthorizationOCPIError from exc + else: + try: + token = decode_string_base64(token) + except UnicodeDecodeError: + pass + return await authenticator.authenticate_credentials(token) + + +class VersionsAuthorizationVerifier(CredentialsAuthorizationVerifier): + """ + A class responsible for verifying authorization tokens + based on the specified version number. + """ + + async def __call__( + self, + authorization: str = auth_verifier, + authenticator: Authenticator = Depends(get_authenticator), + ) -> Union[str, dict, None]: + """ + Verifies the authorization token using the specified version + and an Authenticator for version endpoints. + + :param authorization (str): The authorization header containing + the token. + :param authenticator (Authenticator): An Authenticator instance used + for authentication. + + :raises AuthorizationOCPIError: If there is an issue with + the authorization token. + """ + if settings.NO_AUTH and authorization == "": + logger.debug("Authentication skipped due to NO_AUTH setting.") + return "" + return await super().__call__(authorization, authenticator) + + +class HttpPushVerifier: + """ + A class responsible for verifying authorization tokens if using push. + """ + + async def __call__( + self, + authorization: str = Header(...) if not settings.NO_AUTH else "", + version: VersionNumber = Path(...), + authenticator: Authenticator = Depends(get_authenticator), + ): + """ + Verifies the authorization token using the specified version + and an Authenticator. + + :param authorization (str): The authorization header containing + the token. + :param version (VersionNumber): The authorization header containing + the token. + :param authenticator (Authenticator): An Authenticator instance used + for authentication. + + :raises AuthorizationOCPIError: If there is an issue with + the authorization token. + """ + if settings.NO_AUTH and authorization == "": + logger.debug("Authentication skipped due to NO_AUTH setting.") + return True + + try: + token = authorization.split()[1] + if version.value.startswith("2.2"): + try: + token = decode_string_base64(token) + except UnicodeDecodeError as exc: + logger.debug( + f"Token `{token}` cannot be decoded. " + "Check if the token is already encoded." + ) + raise AuthorizationOCPIError from exc + await authenticator.authenticate(token) + except IndexError as exc: + logger.debug( + "Token cannot be split in parts. " + "Check if it starts with `Token `" + ) + raise AuthorizationOCPIError from exc + + +class WSPushVerifier: + """ + A class responsible for verifying authorization tokens if using ws push. + """ + + async def __call__( + self, + token: str = Query(...) if not settings.NO_AUTH else "", + version: VersionNumber = Path(...), + authenticator: Authenticator = Depends(get_authenticator), + ): + """ + Verifies the authorization token using the specified version + and an Authenticator. + + :param token (str): Token parameter in ws. + :param version (str): The authorization header containing + the token. + :param authenticator (Authenticator): An Authenticator instance used + for authentication. + + :raises AuthorizationOCPIError: If there is an issue with + the authorization token. + """ + if settings.NO_AUTH and token == "": # nosec + logger.debug("Authentication skipped due to NO_AUTH setting.") + return True + + try: + if not token: + logger.debug("Token wasn't given.") + raise AuthorizationOCPIError + + if version.value.startswith("2.2"): + try: + token = decode_string_base64(token) + except UnicodeDecodeError as exc: + logger.debug( + f"Token `{token}` cannot be decoded. " + "Check if the token is already encoded." + ) + raise AuthorizationOCPIError from exc + await authenticator.authenticate(token) + except AuthorizationOCPIError as exc: + raise HTTPException(status_code=status.WS_1008_POLICY_VIOLATION) from exc diff --git a/py_ocpi/core/config.py b/py_ocpi/core/config.py index 762c6d4..e766bf8 100644 --- a/py_ocpi/core/config.py +++ b/py_ocpi/core/config.py @@ -2,19 +2,29 @@ from pydantic import AnyHttpUrl, BaseSettings, validator +from py_ocpi.core.logs import LoggingConfig, logger + class Settings(BaseSettings): - PROJECT_NAME: str = 'OCPI' + ENVIRONMENT: str = "production" + NO_AUTH: bool = False + PROJECT_NAME: str = "OCPI" BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] - OCPI_HOST: str = 'www.example.com' - OCPI_PREFIX: str = 'ocpi' - PUSH_PREFIX: str = 'push' - COUNTRY_CODE: str = 'US' - PARTY_ID: str = 'NON' + OCPI_HOST: str = "www.example.com" + OCPI_PREFIX: str = "ocpi" + PUSH_PREFIX: str = "push" + COUNTRY_CODE: str = "US" + PARTY_ID: str = "NON" + PROTOCOL: str = "https" + COMMAND_AWAIT_TIME: int = 5 + GET_ACTIVE_PROFILE_AWAIT_TIME: int = 5 + TRAILING_SLASH: bool = True @classmethod @validator("BACKEND_CORS_ORIGINS", pre=True) - def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: + def assemble_cors_origins( + cls, v: Union[str, List[str]] + ) -> Union[List[str], str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] if isinstance(v, (list, str)): @@ -27,3 +37,6 @@ class Config: settings = Settings() + +logging_config = LoggingConfig(settings.ENVIRONMENT, logger) +logging_config.configure_logger() diff --git a/py_ocpi/core/crud.py b/py_ocpi/core/crud.py index 1b40942..b993316 100644 --- a/py_ocpi/core/crud.py +++ b/py_ocpi/core/crud.py @@ -1,125 +1,170 @@ -from typing import Any, Tuple +from typing import Any, Tuple, Optional +from abc import ABC, abstractmethod from py_ocpi.core.enums import ModuleID, RoleEnum, Action -class Crud: - @classmethod - async def get(cls, module: ModuleID, role: RoleEnum, id, *args, **kwargs) -> Any: +class Crud(ABC): + @abstractmethod + async def get( + cls, module: ModuleID, role: RoleEnum, id, *args, **kwargs + ) -> Any: """Get an object - Args: - module (ModuleID): The OCPI module - role (RoleEnum): The role of the caller - id (Any): The ID of the object - - Keyword Args: - auth_token (str): The authentication token used by third party - version (VersionNumber): The version number of the caller OCPI module - party_id (CiString(3)): The requested party ID - country_code (CiString(2)): The requested Country code - token_type (TokenType): The token type - command (CommandType): The command type of the OCPP command - command_data (ReserveNow, CancelReservation, StartSession, StopSession, UnlockConnector): The requested - command data - - Returns: - Any: The object data + :param module: The OCPI module + :param role: The role of the caller + :param id: The ID of the object + + :keyword auth_token: (str) The authentication token used by a third + party + :keyword version: (VersionNumber) The version number of the caller + OCPI module + :keyword party_id: (CiString(3)) The requested party ID + :keyword country_code: (CiString(2)) The requested Country code + :keyword token_type: (TokenType) The token type + :keyword command: (CommandType) The command type of the OCPP command + :keyword command_data: Request body of command. + :keyword session_id: (str) Id of the charging profile corresponding + session. + :keyword duration: (int) Length of the requested ActiveChargingProfile + in seconds. + :keyword response_url: (str) Url where to send result of the action. + :keyword charging_profile: (SetChargingProfile) ChargingProfile body. + :keyword session_id: (str) Id of the charging profile corresponding + session. + + :return: The object data + :rtype: Any """ + pass - @classmethod - async def list(cls, module: ModuleID, role: RoleEnum, filters: dict, *args, **kwargs) -> Tuple[list, int, bool]: + @abstractmethod + async def list( + cls, module: ModuleID, role: RoleEnum, filters: dict, *args, **kwargs + ) -> Tuple[list, int, bool]: """Get the list of objects - Args: - module (ModuleID): The OCPI module - role (RoleEnum): The role of the caller - filters (dict): OCPI pagination filters + :param module: The OCPI module + :param role: The role of the caller + :param filters: OCPI pagination filters - Keyword Args: - auth_token (str): The authentication token used by third party - version (VersionNumber): The version number of the caller OCPI module - party_id (CiString(3)): The requested party ID - country_code (CiString(2)): The requested Country code + :keyword auth_token: (str) The authentication token used by a third + party + :keyword version: (VersionNumber) The version number of the caller + OCPI module + :keyword party_id: (CiString(3)) The requested party ID + :keyword country_code: (CiString(2)) The requested Country code - Returns: - Tuple[list, int, bool]: Objects list, Total number of objects, if it's the last page or not(for pagination) + :return: Objects list, Total number of objects, if + it's the last page or not(for pagination) + :rtype: Tuple[list, int, bool] """ + pass - @classmethod - async def create(cls, module: ModuleID, role: RoleEnum, data: dict, *args, **kwargs) -> Any: + @abstractmethod + async def create( + cls, module: ModuleID, role: RoleEnum, data: dict, *args, **kwargs + ) -> Any: """Create an object - Args: - module (ModuleID): The OCPI module - role (RoleEnum): The role of the caller - data (dict): The object details - - Keyword Args: - auth_token (str): The authentication token used by third party - version (VersionNumber): The version number of the caller OCPI module - command (CommandType): The command type (used in Commands module) - party_id (CiString(3)): The requested party ID - country_code (CiString(2)): The requested Country code - token_type (TokenType): The token type - operation ('credentials', 'registration'): The operation type in credentials and registration process - - Returns: - Any: The created object data + :param module: The OCPI module + :param role: The role of the caller + :param data: The object details + + :keyword auth_token: (str) The authentication token used by + a third party + :keyword version: (VersionNumber) The version number of the caller + OCPI module + :keyword party_id: (CiString(3)) The requested party ID + :keyword country_code: (CiString(2)) The requested Country code + :keyword token_type: (TokenType) The token type + :keyword command: (CommandType) The command type of the OCPP command + :keyword query_params: (dict) Charging profile request query params. + + :return: The created object data + :rtype: Any """ - - @classmethod - async def update(cls, module: ModuleID, role: RoleEnum, data: dict, id, *args, **kwargs) -> Any: + pass + + @abstractmethod + async def update( + cls, + module: ModuleID, + role: RoleEnum, + data: dict, + id: Any, + *args, + **kwargs, + ) -> Any: """Update an object - Args: - module (ModuleID): The OCPI module - role (RoleEnum): The role of the caller - data (dict): The object details - id (Any): The ID of the object - - Keyword Args: - auth_token (str): The authentication token used by third party - version (VersionNumber): The version number of the caller OCPI module - party_id (CiString(3)): The requested party ID - country_code (CiString(2)): The requested Country code - token_type (TokenType): The token type - operation ('credentials', 'registration'): The operation type in credentials and registration process - - - Returns: - Any: The updated object data + :param module: The OCPI module + :param role: The role of the caller + :param data: The object details + :param id: The ID of the object + + :keyword auth_token: (str) The authentication token used by a third + party + :keyword version: (VersionNumber) The version number of the caller + OCPI module + :keyword party_id: (CiString(3)) The requested party ID + :keyword country_code: (CiString(2)) The requested Country code + :keyword token_type: (TokenType) The token type + :keyword session_id: (str) Charging profile corresponding session id. + + :return: The updated object data + :rtype: Any """ + pass - @classmethod - async def delete(cls, module: ModuleID, role: RoleEnum, id, *args, **kwargs): + @abstractmethod + async def delete( + cls, module: ModuleID, role: RoleEnum, id, *args, **kwargs + ): """Delete an object - Args: - module (ModuleID): The OCPI module - role (RoleEnum): The role of the caller - id (Any): The ID of the object + :param module: The OCPI module + :param role: The role of the caller + :param id: The ID of the object - Keyword Args: - auth_token (str): The authentication token used by third party - version (VersionNumber): The version number of the caller OCPI module + :keyword auth_token: (str) The authentication token used by a third + party + :keyword version: (VersionNumber) The version number of the caller + OCPI module """ - - @classmethod - async def do(cls, module: ModuleID, role: RoleEnum, action: Action, *args, data: dict = None, **kwargs) -> Any: + pass + + @abstractmethod + async def do( + cls, + module: ModuleID, + role: Optional[RoleEnum], + action: Action, + *args, + data: Optional[dict] = None, + **kwargs, + ) -> Any: """Do an action (non-CRUD) - Args: - module (ModuleID): The OCPI module - role (RoleEnum): The role of the caller - action (Action): The action type - data (dict): The data required for the action - command (CommandType): The command type of the OCPP command - - Keyword Args: - auth_token (str): The authentication token used by third party - version (VersionNumber): The version number of the caller OCPI module - - Returns: - Any: The action result + :param module: The OCPI module + :param role: The role of the caller + :param action: The action type + :param data: The data required for the action + + :keyword auth_token: (str) The authentication token used by a third + party + :keyword version: (VersionNumber) The version number of the caller + OCPI module + :keyword response_url: (str) Response url for actions which require + sending response. + :keyword session: (Session) Session of charging profile action. + :keyword duration: (int) Length of the requested ActiveChargingProfile + in seconds. + :keyword command: (CommandType) The command type of the OCPP command + :keyword charging_profile (SetChargingProfile): Charging profile sent + to be updated. + + :return: The action result + :rtype: Any """ + pass diff --git a/py_ocpi/core/data_types.py b/py_ocpi/core/data_types.py index 488630f..2740244 100644 --- a/py_ocpi/core/data_types.py +++ b/py_ocpi/core/data_types.py @@ -4,15 +4,16 @@ from datetime import datetime from typing import Type - from pydantic.fields import ModelField class StringBase(str): """ Case sensitive String. Only printable UTF-8 allowed. - (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc are not allowed) + (Non-printable characters like: + Carriage returns, Tabs, Line breaks, etc are not allowed) """ + max_length: int @classmethod @@ -22,35 +23,39 @@ def __get_validators__(cls): @classmethod def __modify_schema__(cls, field_schema): field_schema.update( - examples=['String'], + examples=["String"], ) @classmethod def validate(cls, v, field: ModelField): if not isinstance(v, str): - raise TypeError(f'excpected string but received {type(v)}') + raise TypeError(f"excpected string but received {type(v)}") try: - v.encode('UTF-8') + v.encode("UTF-8") except UnicodeError as e: - raise ValueError('invalid string format') from e + raise ValueError("invalid string format") from e if len(v) > cls.max_length: - raise ValueError(f'{field.name} length must be lower or equal to {cls.max_length}') + raise ValueError( + f"{field.name} length must be lower or equal to {cls.max_length}" + ) return cls(v) def __repr__(self): - return f'String({super().__repr__()})' + return f"String({super().__repr__()})" class String: - def __new__(cls, max_length: int = 255) -> Type[str]: - return type('String', (StringBase,), {'max_length': max_length}) + def __new__(cls, max_length: int = 255) -> Type[str]: # type: ignore + return type("String", (StringBase,), {"max_length": max_length}) class CiStringBase(str): """ Case Insensitive String. Only printable ASCII allowed. - (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc are not allowed) + (Non-printable characters like: + Carriage returns, Tabs, Line breaks, etc are not allowed) """ + max_length: int @classmethod @@ -60,32 +65,36 @@ def __get_validators__(cls): @classmethod def __modify_schema__(cls, field_schema): field_schema.update( - examples=['string'], + examples=["string"], ) @classmethod def validate(cls, v, field: ModelField): if not isinstance(v, str): - raise TypeError(f'excpected string but received {type(v)}') + raise TypeError(f"excpected string but received {type(v)}") if not v.isascii(): - raise ValueError('invalid cistring format') + raise ValueError("invalid cistring format") if len(v) > cls.max_length: - raise ValueError(f'{field.name} length must be lower or equal to {cls.max_length}') + raise ValueError( + f"{field.name} length must be lower or equal to {cls.max_length}" + ) return cls(v.lower()) def __repr__(self): - return f'CiString({super().__repr__()})' + return f"CiString({super().__repr__()})" class CiString: - def __new__(cls, max_length: int = 255) -> Type[str]: - return type('CiString', (CiStringBase,), {'max_length': max_length}) + def __new__(cls, max_length: int = 255) -> type: # type: ignore + return type("CiString", (CiStringBase,), {"max_length": max_length}) class URL(str): """ - An URL a String(255) type following the http://www.w3.org/Addressing/URL/uri-spec.html + An URL a String(255) type following the + http://www.w3.org/Addressing/URL/uri-spec.html """ + @classmethod def __get_validators__(cls): yield cls.validate @@ -93,25 +102,27 @@ def __get_validators__(cls): @classmethod def __modify_schema__(cls, field_schema): field_schema.update( - examples=['http://www.w3.org/Addressing/URL/uri-spec.html'], + examples=["http://www.w3.org/Addressing/URL/uri-spec.html"], ) @classmethod def validate(cls, v, field: ModelField): - v = String(255).validate(v, field) + v = String(255).validate(v, field) # type: ignore return cls(v) def __repr__(self): - return f'URL({super().__repr__()})' + return f"URL({super().__repr__()})" class DateTime(str): """ - All timestamps are formatted as string(25) following RFC 3339, with some additional limitations. + All timestamps are formatted as string(25) following RFC 3339, + with some additional limitations. All timestamps SHALL be in UTC. The absence of the timezone designator implies a UTC timestamp. Fractional seconds MAY be used. """ + @classmethod def __get_validators__(cls): yield cls.validate @@ -128,7 +139,7 @@ def validate(cls, v): return cls(formated_v) def __repr__(self): - return f'DateTime({super().__repr__()})' + return f"DateTime({super().__repr__()})" class DisplayText(dict): @@ -139,28 +150,23 @@ def __get_validators__(cls): @classmethod def __modify_schema__(cls, field_schema): field_schema.update( - examples=[ - { - "language": "en", - "text": "Standard Tariff" - } - ], + examples=[{"language": "en", "text": "Standard Tariff"}], ) @classmethod def validate(cls, v): if not isinstance(v, dict): - raise TypeError(f'excpected dict but received {type(v)}') - if 'language' not in v: + raise TypeError(f"excpected dict but received {type(v)}") + if "language" not in v: raise TypeError('property "language" required') - if 'text' not in v: + if "text" not in v: raise TypeError('property "text" required') - if len(v['text']) > 512: - raise TypeError('text too long') + if len(v["text"]) > 512: + raise TypeError("text too long") return cls(v) def __repr__(self): - return f'DateTime({super().__repr__()})' + return f"DateTime({super().__repr__()})" class Number(float): @@ -177,11 +183,11 @@ def __modify_schema__(cls, field_schema): @classmethod def validate(cls, v): if not any([isinstance(v, float), isinstance(v, int)]): - TypeError(f'excpected float but received {type(v)}') + raise TypeError(f"excpected float but received {type(v)}") return cls(float(v)) def __repr__(self): - return f'Number({super().__repr__()})' + return f"Number({super().__repr__()})" class Price(dict): @@ -192,23 +198,18 @@ def __get_validators__(cls): @classmethod def __modify_schema__(cls, field_schema): field_schema.update( - examples=[ - { - 'excl_vat': 1.0000, - 'incl_vat': 1.2500 - } - ], + examples=[{"excl_vat": 1.0000, "incl_vat": 1.2500}], ) @classmethod def validate(cls, v): if not isinstance(v, dict): - raise TypeError('dictionary required') - if 'excl_vat' not in v: + raise TypeError("dictionary required") + if "excl_vat" not in v: raise TypeError('property "excl_vat" required') - if 'incl_vat' not in v: + if "incl_vat" not in v: raise TypeError('property "incl_vat" required') return cls(v) def __repr__(self): - return f'Price({super().__repr__()})' + return f"Price({super().__repr__()})" diff --git a/py_ocpi/core/dependencies.py b/py_ocpi/core/dependencies.py index 9904afd..a98ee34 100644 --- a/py_ocpi/core/dependencies.py +++ b/py_ocpi/core/dependencies.py @@ -3,6 +3,7 @@ from fastapi import Query from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.authenticator import Authenticator from py_ocpi.core.config import settings from py_ocpi.core.crud import Crud from py_ocpi.core.data_types import URL @@ -18,11 +19,18 @@ def get_adapter(): return Adapter +def get_authenticator(): + return Authenticator + + 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.value}/details') + url=URL( + f"https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/" + f"{VersionNumber.v_2_2_1.value}/details" + ), ).dict(), ] @@ -31,15 +39,19 @@ def get_endpoints(): return {} +def get_modules(): + return [] + + def pagination_filters( date_from: datetime = Query(default=None), - date_to: datetime = Query(default=datetime.now()), + date_to: datetime = Query(default=None), offset: int = Query(default=0), limit: int = Query(default=50), ): return { - 'date_from': date_from, - 'date_to': date_to, - 'offset': offset, - 'limit': limit, + "date_from": date_from, + "date_to": date_to, + "offset": offset, + "limit": limit, } diff --git a/py_ocpi/core/endpoints.py b/py_ocpi/core/endpoints.py deleted file mode 100644 index 6ec48ab..0000000 --- a/py_ocpi/core/endpoints.py +++ /dev/null @@ -1,108 +0,0 @@ -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: { - # ###############--CPO--############### - 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--############### - 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/endpoints/__init__.py b/py_ocpi/core/endpoints/__init__.py new file mode 100644 index 0000000..1dec815 --- /dev/null +++ b/py_ocpi/core/endpoints/__init__.py @@ -0,0 +1,9 @@ +from py_ocpi.modules.versions.schemas import VersionNumber + +from .v_2_2_1 import ENDPOINTS_DICT as V_2_2_1_ENDPOINTS_DICT +from .v_2_1_1 import ENDPOINTS_DICT as V_2_1_1_ENDPOINTS_DICT + +ENDPOINTS: dict[str, dict] = { + VersionNumber.v_2_2_1: V_2_2_1_ENDPOINTS_DICT, + VersionNumber.v_2_1_1: V_2_1_1_ENDPOINTS_DICT, +} diff --git a/py_ocpi/core/endpoints/utils.py b/py_ocpi/core/endpoints/utils.py new file mode 100644 index 0000000..3b01f25 --- /dev/null +++ b/py_ocpi/core/endpoints/utils.py @@ -0,0 +1,56 @@ +from abc import abstractmethod + +from py_ocpi.core.config import settings +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.modules.versions.schemas import VersionNumber + + +class URLBuilder: + """Base endpoint generator""" + + def __init__(self): + self.protocol = settings.PROTOCOL + self.ocpi_host = settings.OCPI_HOST + self.ocpi_prefix = settings.OCPI_PREFIX + self.trailing_slash = "/" if settings.TRAILING_SLASH else "" + + def format_url( + self, + version: VersionNumber, + role: RoleEnum, + module: ModuleID, + ) -> str: + """ + Return formatted url for endpoint. + + :param version: OCPI version. + :param role: Role type. + :param module: Module type. + """ + if module == ModuleID.hub_client_info: + module = ModuleID.client_info + return ( + f"{self.protocol}://{self.ocpi_host}/{self.ocpi_prefix}/" + f"{role.value.lower()}/{version.value}/{module.value}" + f"{self.trailing_slash}" + ) + + +class BaseEndpointGenerator(URLBuilder): + """ + Base endpoint generator providing common + functionality for endpoint generation. + + :param version: The OCPI version. + :param role: The role type. + """ + + def __init__(self, version: VersionNumber, role: RoleEnum) -> None: + super().__init__() + self.version = version + self.role = role + + @abstractmethod + def generate_endpoint(self, *args, **kwargs): + """Abstract method for generating specific endpoints.""" + pass diff --git a/py_ocpi/core/endpoints/v_2_1_1/__init__.py b/py_ocpi/core/endpoints/v_2_1_1/__init__.py new file mode 100644 index 0000000..4a2b44f --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_1_1/__init__.py @@ -0,0 +1,9 @@ +from py_ocpi.core.enums import RoleEnum + +from .cpo import ENDPOINTS_LIST as CPO_ENDPOINTS_LIST +from .emsp import ENDPOINTS_LIST as EMSP_ENDPOINTS_LIST + +ENDPOINTS_DICT = { + RoleEnum.cpo: CPO_ENDPOINTS_LIST, + RoleEnum.emsp: EMSP_ENDPOINTS_LIST, +} diff --git a/py_ocpi/core/endpoints/v_2_1_1/cpo.py b/py_ocpi/core/endpoints/v_2_1_1/cpo.py new file mode 100644 index 0000000..0fda20a --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_1_1/cpo.py @@ -0,0 +1,26 @@ +from py_ocpi.core.enums import ModuleID +from py_ocpi.core.endpoints.v_2_1_1.utils import cpo_generator + + +CREDENTIALS_AND_REGISTRATION = cpo_generator.generate_endpoint( + ModuleID.credentials_and_registration, +) + +LOCATIONS = cpo_generator.generate_endpoint(ModuleID.locations) + +CDRS = cpo_generator.generate_endpoint(ModuleID.cdrs) + +TARIFFS = cpo_generator.generate_endpoint(ModuleID.tariffs) + +SESSIONS = cpo_generator.generate_endpoint(ModuleID.sessions) + +TOKENS = cpo_generator.generate_endpoint(ModuleID.tokens) + +ENDPOINTS_LIST = { + ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, + ModuleID.locations: LOCATIONS, + ModuleID.cdrs: CDRS, + ModuleID.tariffs: TARIFFS, + ModuleID.sessions: SESSIONS, + ModuleID.tokens: TOKENS, +} diff --git a/py_ocpi/core/endpoints/v_2_1_1/emsp.py b/py_ocpi/core/endpoints/v_2_1_1/emsp.py new file mode 100644 index 0000000..ac9b518 --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_1_1/emsp.py @@ -0,0 +1,29 @@ +from py_ocpi.core.enums import ModuleID +from py_ocpi.core.endpoints.v_2_1_1.utils import emsp_generator + + +CREDENTIALS_AND_REGISTRATION = emsp_generator.generate_endpoint( + ModuleID.credentials_and_registration, +) + +LOCATIONS = emsp_generator.generate_endpoint(ModuleID.locations) + +CDRS = emsp_generator.generate_endpoint(ModuleID.cdrs) + +TARIFFS = emsp_generator.generate_endpoint(ModuleID.tariffs) + +SESSIONS = emsp_generator.generate_endpoint(ModuleID.sessions) + +TOKENS = emsp_generator.generate_endpoint(ModuleID.tokens) + +COMMANDS = emsp_generator.generate_endpoint(ModuleID.commands) + +ENDPOINTS_LIST = { + ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, + ModuleID.locations: LOCATIONS, + ModuleID.cdrs: CDRS, + ModuleID.tariffs: TARIFFS, + ModuleID.sessions: SESSIONS, + ModuleID.tokens: TOKENS, + ModuleID.commands: COMMANDS, +} diff --git a/py_ocpi/core/endpoints/v_2_1_1/utils.py b/py_ocpi/core/endpoints/v_2_1_1/utils.py new file mode 100644 index 0000000..c77213c --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_1_1/utils.py @@ -0,0 +1,53 @@ +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.endpoints.utils import BaseEndpointGenerator +from py_ocpi.core.data_types import URL +from py_ocpi.modules.versions.v_2_1_1.schemas import ( + Endpoint, + VersionNumber, +) + + +class BaseEndpointGenerator211(BaseEndpointGenerator): + """ + Endpoint generator which uses Endpoint schema v2.1.1 + + :param role: The role type. + """ + + def __init__(self, role: RoleEnum) -> None: + self.version = VersionNumber.v_2_1_1 + super().__init__(version=self.version, role=role) + + def generate_endpoint( + self, + module: ModuleID, + *args, + **kwargs, + ) -> Endpoint: + """ + Return generated Endpoint schema. + + :param module: Module type. + """ + url = self.format_url(self.version, self.role, module) + return Endpoint(identifier=module, url=URL(url)) + + +class CPOEndpointGenerator211(BaseEndpointGenerator211): + """Endpoint generator for CPO role using Endpoint schema v2.1.1.""" + + def __init__(self): + self.role = RoleEnum.cpo + super().__init__(role=self.role) + + +class EMSPEndpointGenerator211(BaseEndpointGenerator211): + """Endpoint generator for EMSP role using Endpoint schema v2.1.1.""" + + def __init__(self): + self.role = RoleEnum.emsp + super().__init__(role=self.role) + + +cpo_generator = CPOEndpointGenerator211() +emsp_generator = EMSPEndpointGenerator211() diff --git a/py_ocpi/core/endpoints/v_2_2_1/__init__.py b/py_ocpi/core/endpoints/v_2_2_1/__init__.py new file mode 100644 index 0000000..4a2b44f --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_2_1/__init__.py @@ -0,0 +1,9 @@ +from py_ocpi.core.enums import RoleEnum + +from .cpo import ENDPOINTS_LIST as CPO_ENDPOINTS_LIST +from .emsp import ENDPOINTS_LIST as EMSP_ENDPOINTS_LIST + +ENDPOINTS_DICT = { + RoleEnum.cpo: CPO_ENDPOINTS_LIST, + RoleEnum.emsp: EMSP_ENDPOINTS_LIST, +} diff --git a/py_ocpi/core/endpoints/v_2_2_1/cpo.py b/py_ocpi/core/endpoints/v_2_2_1/cpo.py new file mode 100644 index 0000000..8eeea60 --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_2_1/cpo.py @@ -0,0 +1,56 @@ +from py_ocpi.core.enums import ModuleID +from py_ocpi.core.endpoints.v_2_2_1.utils import cpo_generator +from py_ocpi.modules.versions.v_2_2_1.schemas import InterfaceRole + + +CREDENTIALS_AND_REGISTRATION = cpo_generator.generate_endpoint( + ModuleID.credentials_and_registration, + InterfaceRole.receiver, +) + +LOCATIONS = cpo_generator.generate_endpoint( + ModuleID.locations, + InterfaceRole.sender, +) + +SESSIONS = cpo_generator.generate_endpoint( + ModuleID.sessions, + InterfaceRole.sender, +) + +CDRS = cpo_generator.generate_endpoint( + ModuleID.cdrs, + InterfaceRole.sender, +) + +TARIFFS = cpo_generator.generate_endpoint( + ModuleID.tariffs, + InterfaceRole.sender, +) + +TOKENS = cpo_generator.generate_endpoint( + ModuleID.tokens, + InterfaceRole.receiver, +) + +HUB_CLIENT_INFO = cpo_generator.generate_endpoint( + ModuleID.hub_client_info, + InterfaceRole.receiver, +) + +CHARGING_PROFILE = cpo_generator.generate_endpoint( + ModuleID.charging_profile, + InterfaceRole.receiver, +) + + +ENDPOINTS_LIST = { + ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, + ModuleID.locations: LOCATIONS, + ModuleID.sessions: SESSIONS, + ModuleID.cdrs: CDRS, + ModuleID.tariffs: TARIFFS, + ModuleID.tokens: TOKENS, + ModuleID.hub_client_info: HUB_CLIENT_INFO, + ModuleID.charging_profile: CHARGING_PROFILE, +} diff --git a/py_ocpi/core/endpoints/v_2_2_1/emsp.py b/py_ocpi/core/endpoints/v_2_2_1/emsp.py new file mode 100644 index 0000000..1735c00 --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_2_1/emsp.py @@ -0,0 +1,61 @@ +from py_ocpi.core.enums import ModuleID +from py_ocpi.core.endpoints.v_2_2_1.utils import emsp_generator +from py_ocpi.modules.versions.v_2_2_1.schemas import InterfaceRole + + +CREDENTIALS_AND_REGISTRATION = emsp_generator.generate_endpoint( + ModuleID.credentials_and_registration, + InterfaceRole.receiver, +) + +LOCATIONS = emsp_generator.generate_endpoint( + ModuleID.locations, + InterfaceRole.receiver, +) + +SESSIONS = emsp_generator.generate_endpoint( + ModuleID.sessions, + InterfaceRole.receiver, +) + +CDRS = emsp_generator.generate_endpoint( + ModuleID.cdrs, + InterfaceRole.receiver, +) + +TARIFFS = emsp_generator.generate_endpoint( + ModuleID.tariffs, + InterfaceRole.receiver, +) + +COMMANDS = emsp_generator.generate_endpoint( + ModuleID.commands, + InterfaceRole.sender, +) + +TOKENS = emsp_generator.generate_endpoint( + ModuleID.tokens, + InterfaceRole.sender, +) + +HUB_CLIENT_INFO = emsp_generator.generate_endpoint( + ModuleID.hub_client_info, + InterfaceRole.receiver, +) + +CHARGING_PROFILE = emsp_generator.generate_endpoint( + ModuleID.charging_profile, + InterfaceRole.sender, +) + +ENDPOINTS_LIST = { + ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, + ModuleID.locations: LOCATIONS, + ModuleID.sessions: SESSIONS, + ModuleID.cdrs: CDRS, + ModuleID.tariffs: TARIFFS, + ModuleID.commands: COMMANDS, + ModuleID.tokens: TOKENS, + ModuleID.hub_client_info: HUB_CLIENT_INFO, + ModuleID.charging_profile: CHARGING_PROFILE, +} diff --git a/py_ocpi/core/endpoints/v_2_2_1/utils.py b/py_ocpi/core/endpoints/v_2_2_1/utils.py new file mode 100644 index 0000000..c50974e --- /dev/null +++ b/py_ocpi/core/endpoints/v_2_2_1/utils.py @@ -0,0 +1,54 @@ +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.endpoints.utils import BaseEndpointGenerator +from py_ocpi.core.data_types import URL +from py_ocpi.modules.versions.v_2_2_1.schemas import ( + Endpoint, + InterfaceRole, + VersionNumber, +) + + +class BaseEndpointGenerator221(BaseEndpointGenerator): + def __init__(self, role: RoleEnum) -> None: + self.version = VersionNumber.v_2_2_1 + super().__init__(version=self.version, role=role) + + def generate_endpoint( + self, + module: ModuleID, + interface_role: InterfaceRole, + *args, + **kwargs, + ) -> Endpoint: + """ + Return generated Endpoint schema. + + :param module: Module type. + :param interface_role: Interface role of endpoint. + """ + url = self.format_url(self.version, self.role, module) + return Endpoint( + identifier=module, + role=interface_role, + url=URL(url), + ) + + +class CPOEndpointGenerator221(BaseEndpointGenerator221): + """Endpoint generator for CPO role using Endpoint schema v2.2.1.""" + + def __init__(self): + self.role = RoleEnum.cpo + super().__init__(role=self.role) + + +class EMSPEndpointGenerator221(BaseEndpointGenerator221): + """Endpoint generator for EMSP role using Endpoint schema v2.2.1.""" + + def __init__(self): + self.role = RoleEnum.emsp + super().__init__(role=self.role) + + +cpo_generator = CPOEndpointGenerator221() +emsp_generator = EMSPEndpointGenerator221() diff --git a/py_ocpi/core/enums.py b/py_ocpi/core/enums.py index 21f59a7..7c33772 100644 --- a/py_ocpi/core/enums.py +++ b/py_ocpi/core/enums.py @@ -5,41 +5,58 @@ class RoleEnum(str, Enum): """ https://github.com/ocpi/ocpi/blob/2.2.1/types.asciidoc#151-role-enum """ + # Charge Point Operator Role - cpo = 'CPO' + cpo = "CPO" # eMobility Service Provider Role - emsp = 'EMSP' + emsp = "EMSP" # Hub role - hub = 'HUB' - # National Access Point Role (national Database with all Location information of a country) - nap = 'NAP' - # Navigation Service Provider Role, role like an eMSP (probably only interested in Location information) - nsp = 'NSP' + hub = "HUB" + # National Access Point Role + # (national Database with all Location information of a country) + nap = "NAP" + # Navigation Service Provider Role, role like an eMSP + # (probably only interested in Location information) + nsp = "NSP" # Other role - other = 'OTHER' + other = "OTHER" # Smart Charging Service Provider Role - scsp = 'SCSP' + scsp = "SCSP" class ModuleID(str, Enum): """ https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#124-moduleid-enum """ - cdrs = 'cdrs' - charging_profile = 'chargingprofiles' - commands = 'commands' - credentials_and_registration = 'credentials' - hub_client_info = 'hubclientinfo' - locations = 'locations' - sessions = 'sessions' - tariffs = 'tariffs' - tokens = 'tokens' + + cdrs = "cdrs" + charging_profile = "chargingprofiles" + commands = "commands" + credentials_and_registration = "credentials" + hub_client_info = "hubclientinfo" + client_info = "clientinfo" + locations = "locations" + sessions = "sessions" + tariffs = "tariffs" + tokens = "tokens" class Action(str, Enum): # used for requesting to send an OCPP command to a Chargepoint - send_command = 'SendCommand' + send_command = "SendCommand" # used for getting client authentication token - get_client_token = 'GetClientToken' # nosec + get_client_token = "GetClientToken" # nosec # used for authorizing a token - authorize_token = 'AuthorizeToken' # nosec + authorize_token = "AuthorizeToken" # nosec + # used for requesting to send command to a Chargepoint + send_get_chargingprofile = "SendGetChargingProfile" # nosec + # used for requesting to send command to a Chargepoint + send_delete_chargingprofile = "SendDeleteChargingProfile" # nosec + # used for requesting to send command to a Chargepoint + send_update_charging_profile = "SendUpdateChargingProfile" # nosec + + +class EnvironmentType(str, Enum): + production = "production" + development = "development" + testing = "testing" diff --git a/py_ocpi/core/exceptions.py b/py_ocpi/core/exceptions.py index 701e75f..976038b 100644 --- a/py_ocpi/core/exceptions.py +++ b/py_ocpi/core/exceptions.py @@ -6,9 +6,9 @@ class OCPIError(Exception): class AuthorizationOCPIError(OCPIError): def __str__(self): - return 'Your authorization token is invalid.' + return "Your authorization token is invalid." class NotFoundOCPIError(OCPIError): def __str__(self): - return 'Object not found.' + return "Object not found." diff --git a/py_ocpi/core/logs.py b/py_ocpi/core/logs.py new file mode 100644 index 0000000..8ba9fb6 --- /dev/null +++ b/py_ocpi/core/logs.py @@ -0,0 +1,52 @@ +"""Logging configuration.""" +import logging + +from py_ocpi.core.enums import EnvironmentType + + +class CustomFormatter(logging.Formatter): + """Custom logging formatter.""" + + grey = "\x1b[36;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + blue = "\x1b[34;20m" + reset = "\x1b[0m" + form = "%(asctime)s | [%(levelname)s] %(message)s (%(filename)s:%(lineno)d)" + + FORMATS = { + logging.INFO: f"{grey}{form}{reset}", + logging.WARNING: f"{yellow}{form}{reset}", + logging.ERROR: f"{red}{form}{reset}", + logging.DEBUG: f"{blue}{form}{reset}", + } + + def format(self, record): + """Return formatted logging message.""" + log_fmt = self.FORMATS.get(record.levelno) # noqa + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +class LoggingConfig: + def __init__(self, environment: str, logger) -> None: + self.environment = environment + self.logger = logger + + def configure_logger(self): + if self.environment == EnvironmentType.production.value: + self.logger.setLevel(logging.INFO) + elif self.environment == EnvironmentType.development.value: + self.logger.setLevel(logging.DEBUG) + elif self.environment == EnvironmentType.testing.value: + self.logger.setLevel(logging.DEBUG) + else: + raise ValueError("Invalid environment") + + +logger = logging.getLogger("OCPI-Logger") + +handler = logging.StreamHandler() +handler.setFormatter(CustomFormatter()) + +logger.addHandler(handler) diff --git a/py_ocpi/core/push.py b/py_ocpi/core/push.py index f7639a3..ffe1f38 100644 --- a/py_ocpi/core/push.py +++ b/py_ocpi/core/push.py @@ -1,40 +1,52 @@ +from typing import Union + import httpx from fastapi import APIRouter, Request, WebSocket, Depends from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import ( + HttpPushVerifier, + WSPushVerifier, +) from py_ocpi.core.crud import Crud from py_ocpi.core.schemas import Push, PushResponse, ReceiverResponse 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 -from py_ocpi.modules.versions.enums import InterfaceRole, VersionNumber +from py_ocpi.core.config import settings, logger +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.versions.v_2_2_1.enums import InterfaceRole def client_url(module_id: ModuleID, object_id: str, base_url: str) -> str: if module_id == ModuleID.cdrs: return base_url - return f'{base_url}/{settings.COUNTRY_CODE}/{settings.PARTY_ID}/{object_id}' + return f"{base_url}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/{object_id}" def client_method(module_id: ModuleID) -> str: if module_id == ModuleID.cdrs: - return 'POST' - return 'PUT' + return "POST" + return "PUT" -def request_data(module_id: ModuleID, object_data: dict, adapter: Adapter) -> dict: +def request_data( + module_id: ModuleID, + object_data: dict, + adapter: Adapter, + version: VersionNumber, +) -> dict: data = {} if module_id == ModuleID.locations: - data = adapter.location_adapter(object_data).dict() + data = adapter.location_adapter(object_data, version).dict() elif module_id == ModuleID.sessions: - data = adapter.session_adapter(object_data).dict() + data = adapter.session_adapter(object_data, version).dict() elif module_id == ModuleID.cdrs: - data = adapter.cdr_adapter(object_data).dict() + data = adapter.cdr_adapter(object_data, version).dict() elif module_id == ModuleID.tariffs: - data = adapter.tariff_adapter(object_data).dict() + data = adapter.tariff_adapter(object_data, version).dict() elif module_id == ModuleID.tokens: - data = adapter.token_adapter(object_data).dict() + data = adapter.token_adapter(object_data, version).dict() return data @@ -45,79 +57,162 @@ async def send_push_request( adapter: Adapter, client_auth_token: str, endpoints: list, + version: VersionNumber, ): - data = request_data(module_id, object_data, adapter) + data = request_data(module_id, object_data, adapter, version) - base_url = '' + base_url = "" for endpoint in endpoints: - if endpoint['identifier'] == module_id and endpoint['role'] == InterfaceRole.receiver: - base_url = endpoint['url'] + if ( + version.value.startswith("2.2") + and endpoint["identifier"] == module_id + and endpoint["role"] == InterfaceRole.receiver + ) or ( + version.value.startswith("2.1") + and endpoint["identifier"] == module_id + ): + base_url = endpoint["url"] # push object to client - async with httpx.AsyncClient() as client: - request = client.build_request(client_method(module_id), client_url(module_id, object_id, base_url), - headers={'authorization': client_auth_token}, json=data) + async with httpx.AsyncClient() as client: # nosec + request = client.build_request( + client_method(module_id), + client_url(module_id, object_id, base_url), + headers={"Authorization": client_auth_token}, + json=data, + ) response = await client.send(request) return response -async def push_object(version: VersionNumber, push: Push, crud: Crud, adapter: Adapter, - auth_token: str = None) -> PushResponse: +async def push_object( + version: VersionNumber, + push: Push, + crud: Crud, + adapter: Adapter, + auth_token: Union[str, None] = None, +) -> PushResponse: receiver_responses = [] for receiver in push.receivers: # get client endpoints - 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}) - endpoints = response.json()['data'][0]['endpoints'] + if version.value.startswith("2.1") or version.value.startswith("2.0"): + token = receiver.auth_token + else: + token = encode_string_base64(receiver.auth_token) + + client_auth_token = f"Token {token}" + + async with httpx.AsyncClient() as client: # nosec + logger.info( + f"Send request to get version details: {receiver.endpoints_url}" + ) + response = await client.get( + receiver.endpoints_url, + headers={"authorization": client_auth_token}, + ) + logger.info(f"Response status_code - `{response.status_code}`") + endpoints = response.json()["data"]["endpoints"] + logger.debug(f"Endpoints response data - `{endpoints}`") # get object data if push.module_id == ModuleID.tokens: - data = await crud.get(push.module_id, RoleEnum.emsp, push.object_id, - auth_token=auth_token, version=version) + logger.debug("Requested module with push is token.") + data = await crud.get( + push.module_id, + RoleEnum.emsp, + push.object_id, + auth_token=auth_token, + version=version, + ) else: - data = await crud.get(push.module_id, RoleEnum.cpo, push.object_id, - auth_token=auth_token, version=version) - - response = await send_push_request(push.object_id, data, push.module_id, adapter, client_auth_token, endpoints) + logger.debug(f"Requested module with push is `{push.module_id}`.") + data = await crud.get( + push.module_id, + RoleEnum.cpo, + push.object_id, + auth_token=auth_token, + version=version, + ) + + response = await send_push_request( + push.object_id, + data, + push.module_id, + adapter, + client_auth_token, + endpoints, + version, + ) if push.module_id == ModuleID.cdrs: - receiver_responses.append(ReceiverResponse(endpoints_url=receiver.endpoints_url, - status_code=response.status_code, - response=response.headers)) + logger.debug("Add headers for CDR module into response.") + receiver_responses.append( + ReceiverResponse( + endpoints_url=receiver.endpoints_url, + status_code=response.status_code, + response=response.headers, + ) + ) else: - receiver_responses.append(ReceiverResponse(endpoints_url=receiver.endpoints_url, - status_code=response.status_code, - response=response.json())) - - return PushResponse(receiver_responses=receiver_responses) + receiver_responses.append( + ReceiverResponse( + endpoints_url=receiver.endpoints_url, + status_code=response.status_code, + response=response.json(), + ) + ) + result = PushResponse(receiver_responses=receiver_responses) + logger.debug(f"Result of push operation - {result.dict()}") + return result -http_router = APIRouter() +http_router = APIRouter( + dependencies=[Depends(HttpPushVerifier())], +) # WARNING it's advised not to expose this endpoint -@http_router.post("/{version}", status_code=200, include_in_schema=False, response_model=PushResponse) -async def http_push_to_client(request: Request, version: VersionNumber, push: Push, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): - auth_token = get_auth_token(request) +@http_router.post( + "/{version}", + status_code=200, + include_in_schema=False, + response_model=PushResponse, +) +async def http_push_to_client( + request: Request, + version: VersionNumber, + push: Push, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + logger.info("Received push http request.") + logger.debug(f"Received push data - `{push.dict()}`") + auth_token = get_auth_token(request, version) return await push_object(version, push, crud, adapter, auth_token) -websocket_router = APIRouter() +websocket_router = APIRouter( + dependencies=[Depends(WSPushVerifier())], +) # WARNING it's advised not to expose this endpoint @websocket_router.websocket("/ws/{version}") -async def websocket_push_to_client(websocket: WebSocket, version: VersionNumber, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): - - auth_token = get_auth_token(websocket) +async def websocket_push_to_client( + websocket: WebSocket, + version: VersionNumber, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + auth_token = get_auth_token(websocket, version) await websocket.accept() while True: data = await websocket.receive_json() + logger.debug(f"Received data through ws - `{data}`") push = Push(**data) - push_response = await push_object(version, push, crud, adapter, auth_token) + push_response = await push_object( + version, push, crud, adapter, auth_token + ) + logger.debug(f"Sending push response - `{push_response.dict()}`") await websocket.send_json(push_response.dict()) diff --git a/py_ocpi/core/routers/__init__.py b/py_ocpi/core/routers/__init__.py new file mode 100644 index 0000000..46a6b4d --- /dev/null +++ b/py_ocpi/core/routers/__init__.py @@ -0,0 +1,10 @@ +from py_ocpi.modules.versions.enums import VersionNumber + +from .v_2_2_1 import ROUTERS_DICT as V_2_2_1_ROUTERS_DICT +from .v_2_1_1 import ROUTERS_DICT as V_2_1_1_ROUTERS_DICT + + +ROUTERS = { + VersionNumber.v_2_2_1: V_2_2_1_ROUTERS_DICT, + VersionNumber.v_2_1_1: V_2_1_1_ROUTERS_DICT, +} diff --git a/py_ocpi/core/routers/v_2_1_1/__init__.py b/py_ocpi/core/routers/v_2_1_1/__init__.py new file mode 100644 index 0000000..8fef7ea --- /dev/null +++ b/py_ocpi/core/routers/v_2_1_1/__init__.py @@ -0,0 +1,8 @@ +from py_ocpi.routers import v_2_1_1_cpo_router, v_2_1_1_emsp_router +from py_ocpi.modules import versions_v_2_1_1_router + +ROUTERS_DICT = { + "version_router": versions_v_2_1_1_router, + "cpo_router": v_2_1_1_cpo_router, + "emsp_router": v_2_1_1_emsp_router, +} diff --git a/py_ocpi/core/routers/v_2_2_1/__init__.py b/py_ocpi/core/routers/v_2_2_1/__init__.py new file mode 100644 index 0000000..2471684 --- /dev/null +++ b/py_ocpi/core/routers/v_2_2_1/__init__.py @@ -0,0 +1,8 @@ +from py_ocpi.routers import v_2_2_1_cpo_router, v_2_2_1_emsp_router +from py_ocpi.modules import versions_v_2_2_1_router + +ROUTERS_DICT = { + "version_router": versions_v_2_2_1_router, + "cpo_router": v_2_2_1_cpo_router, + "emsp_router": v_2_2_1_emsp_router, +} diff --git a/py_ocpi/core/status.py b/py_ocpi/core/status.py index 747eb2d..6b48c4d 100644 --- a/py_ocpi/core/status.py +++ b/py_ocpi/core/status.py @@ -1,67 +1,70 @@ """ -OCPI status codes based on https://github.com/ocpi/ocpi/blob/2.2.1/status_codes.asciidoc +OCPI status codes based on +https://github.com/ocpi/ocpi/blob/2.2.1/status_codes.asciidoc """ # 1xxx: Success OCPI_1000_GENERIC_SUCESS_CODE = { - 'status_code': 1000, - 'status_message': 'Generic success code' + "status_code": 1000, + "status_message": "Generic success code", } # 2xxx: Client errors OCPI_2000_GENERIC_CLIENT_ERROR = { - 'status_code': 2000, - 'status_message': 'Generic client error' + "status_code": 2000, + "status_message": "Generic client error", } OCPI_2001_INVALID_OR_MISSING_PARAMETERS = { - 'status_code': 2001, - 'status_message': 'Invalid or missing parameters' + "status_code": 2001, + "status_message": "Invalid or missing parameters", } OCPI_2002_NOT_ENOUGH_INFORMATION = { - 'status_code': 2002, - 'status_message': 'Not enough information' + "status_code": 2002, + "status_message": "Not enough information", } OCPI_2003_UNKNOWN_LOCATION = { - 'status_code': 2003, - 'status_message': 'Unknown Location' + "status_code": 2003, + "status_message": "Unknown Location", } OCPI_2004_UNKNOWN_TOKEN = { - 'status_code': 2004, - 'status_message': 'Unknown Token' + "status_code": 2004, + "status_message": "Unknown Token", } # 3xxx: Server errors OCPI_3000_GENERIC_SERVER_ERROR = { - 'status_code': 3000, - 'status_message': 'Generic server error' + "status_code": 3000, + "status_message": "Generic server error", } OCPI_3001_UNABLE_TO_USE_CLIENTS_API = { - 'status_code': 3001, - 'status_message': 'Unable to use the client’s API' + "status_code": 3001, + "status_message": "Unable to use the client’s API", } OCPI_3002_UNSUPPORTED_VERSION = { - 'status_code': 3002, - 'status_message': 'Unsupported version' + "status_code": 3002, + "status_message": "Unsupported version", } OCPI_3003_NO_MATCHING_ENDPOINT = { - 'status_code': 3003, - 'status_message': 'No matching endpoints or expected endpoints missing between parties' + "status_code": 3003, + "status_message": "No matching endpoints or " + "expected endpoints missing between parties", } # 4xxx: Hub errors OCPI_4000_GENERIC_ERROR = { - 'status_code': 4000, - 'status_message': 'Generic error' + "status_code": 4000, + "status_message": "Generic error", } OCPI_4001_UNKNOWN_RECEIVER = { - 'status_code': 4001, - 'status_message': 'Unknown receiver (TO address is unknown)' + "status_code": 4001, + "status_message": "Unknown receiver (TO address is unknown)", } OCPI_4002_TIMEOUT_ON_FORWARDED_REQUEST = { - 'status_code': 4002, - 'status_message': 'Timeout on forwarded request (message is forwarded, but request times out)' + "status_code": 4002, + "status_message": "Timeout on forwarded request " + "(message is forwarded, but request times out)", } OCPI_4003_CONNECTION_PROBLEM = { - 'status_code': 4003, - 'status_message': 'Connection problem (receiving party is not connected)' + "status_code": 4003, + "status_message": "Connection problem (receiving party is not connected)", } diff --git a/py_ocpi/core/utils.py b/py_ocpi/core/utils.py index 6655490..94d5631 100644 --- a/py_ocpi/core/utils.py +++ b/py_ocpi/core/utils.py @@ -1,43 +1,69 @@ +import importlib import urllib import base64 +from typing import Union, Any from fastapi import Response, Request from pydantic import BaseModel +from py_ocpi.core.config import logger from py_ocpi.core.enums import ModuleID, RoleEnum from py_ocpi.core.config import settings from py_ocpi.modules.versions.enums import VersionNumber -def set_pagination_headers(response: Response, link: str, total: int, limit: int): - response.headers['Link'] = link - response.headers['X-Total-Count'] = str(total) - response.headers['X-Limit'] = str(limit) +def set_pagination_headers( + response: Response, link: str, total: int, limit: int +): + response.headers["Link"] = link + response.headers["X-Total-Count"] = str(total) + response.headers["X-Limit"] = str(limit) return response -def get_auth_token(request: Request) -> str: +def get_auth_token( + request: Request, + version: VersionNumber = VersionNumber.v_2_2_1, +) -> Union[str, None]: headers = request.headers - headers_token = headers.get('authorization', 'Token Null') + headers_token = headers.get("authorization", "Token Null") token = headers_token.split()[1] - if token == 'Null': # nosec + if token == "Null": # nosec return None + if version.startswith("2.1") or version.startswith("2.0"): + return token return decode_string_base64(token) -async def get_list(response: Response, filters: dict, module: ModuleID, role: RoleEnum, - version: VersionNumber, crud, *args, **kwargs): - data_list, total, is_last_page = await crud.list(module, role, filters, *args, version=version, **kwargs) - - link = '' - params = dict(**filters) - params['offset'] = filters['offset'] + filters['limit'] +async def get_list( + response: Response, + filters: dict, + module: ModuleID, + role: RoleEnum, + version: VersionNumber, + crud, + *args, + **kwargs, +): + data_list, total, is_last_page = await crud.list( + module, role, filters, *args, version=version, **kwargs + ) + + link = "" + params = {**filters} + params["offset"] = filters["offset"] + filters["limit"] if not is_last_page: - link = (f'; rel="next"') - - set_pagination_headers(response, link, total, filters['limit']) - + link = ( + f"; rel="next"' # type: ignore + ) + + set_pagination_headers(response, link, total, filters["limit"]) + logger.debug( + f"List / total / is_last_page -> " + f"{len(data_list)} / {total} / {is_last_page}." + ) return data_list @@ -47,10 +73,21 @@ def partially_update_attributes(instance: BaseModel, attributes: dict): def encode_string_base64(input: str) -> str: - input_bytes = base64.b64encode(bytes(input, 'utf-8')) - return input_bytes.decode('utf-8') + 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') + input_bytes = base64.b64decode(bytes(input, "utf-8")) + return input_bytes.decode("utf-8") + + +def get_module_model(class_name, module_name: str, version_name: str) -> Any: + module_dir = f"py_ocpi.modules.{module_name}.{version_name}.schemas" + try: + module = importlib.import_module(module_dir) + return getattr(module, class_name) + except ImportError as exc: + raise NotImplementedError( + f"{class_name} schema for version {version_name} not found.", + ) from exc diff --git a/py_ocpi/main.py b/py_ocpi/main.py index 8889b9a..47164e1 100644 --- a/py_ocpi/main.py +++ b/py_ocpi/main.py @@ -1,59 +1,116 @@ from typing import Any, List -from fastapi import FastAPI, Request, HTTPException +from fastapi import FastAPI, Request, status as fastapistatus from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import ValidationError -from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +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.main import router as versions_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, get_endpoints +from py_ocpi.core.dependencies import ( + get_crud, + get_adapter, + get_versions, + get_endpoints, + get_modules, + get_authenticator, +) from py_ocpi.core import status -from py_ocpi.core.enums import RoleEnum -from py_ocpi.core.config import settings +from py_ocpi.core.adapter import BaseAdapter +from py_ocpi.core.enums import RoleEnum, ModuleID +from py_ocpi.core.config import settings, logger from py_ocpi.core.data_types import URL from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.exceptions import AuthorizationOCPIError, NotFoundOCPIError -from py_ocpi.core.push import http_router as http_push_router, websocket_router as websocket_push_router -from py_ocpi.routers import v_2_2_1_cpo_router, v_2_2_1_emsp_router +from py_ocpi.core.push import ( + http_router as http_push_router, + websocket_router as websocket_push_router, +) +from py_ocpi.core.routers import ROUTERS class ExceptionHandlerMiddleware(BaseHTTPMiddleware): - async def dispatch( self, request: Request, call_next: RequestResponseEndpoint ): + logger.debug(f"{request.method}: {request.url}") + logger.debug(f"Request headers - {request.headers}") + try: response = await call_next(request) except AuthorizationOCPIError as e: - raise HTTPException(403, str(e)) from e + logger.warning("OCPI middleware AuthorizationOCPIError exception.") + response = JSONResponse( + content={"detail": str(e)}, + status_code=fastapistatus.HTTP_403_FORBIDDEN, + ) except NotFoundOCPIError as e: - raise HTTPException(404, str(e)) from e + logger.warning("OCPI middleware NotFoundOCPIError exception.") + response = JSONResponse( + content={"detail": str(e)}, + status_code=fastapistatus.HTTP_404_NOT_FOUND, + ) except ValidationError: + logger.warning("OCPI middleware ValidationError exception.") + response = JSONResponse( + OCPIResponse( + data=[], + **status.OCPI_3000_GENERIC_SERVER_ERROR, + ).dict() + ) + except Exception as e: # pylint: disable=broad-exception-caught + logger.warning(f"Unknown exception: {str(e)}.") response = JSONResponse( OCPIResponse( data=[], **status.OCPI_3000_GENERIC_SERVER_ERROR, ).dict() ) + + logger.debug(f"Response status_code -> {response.status_code}.") return response -def get_application( +def get_application( # noqa: MC0001 version_numbers: List[VersionNumber], roles: List[RoleEnum], crud: Any, - adapter: Any, + modules: List[ModuleID], + authenticator: Any, + adapter: Any = BaseAdapter, http_push: bool = False, websocket_push: bool = False, ) -> FastAPI: + """ + OCPI application initializer. + + :param version_numbers: List of version numbers which are supported. + :param roles: Roles which are supported. + :param crud: Class with crud methods which should contain business logic + and db methods. + :param modules: OCPI modules which should be supported. [Some modules are + related, make sure to check OCPI documentation first.] + :param authenticator: Authenticator class, which would check validity of + authentication tokens. + :param adapter: Model to dict data transformer. + :param http_push: If True, add endpoint where the command to send to + corresponding client data update could be made. + :param websocket_push: If True, add websocket endpoint where data updates + will be shared. + + :return: FastApi application. + """ _app = FastAPI( title=settings.PROJECT_NAME, - docs_url=f'/{settings.OCPI_PREFIX}/docs', - openapi_url=f"/{settings.OCPI_PREFIX}/openapi.json" + docs_url=f"/{settings.OCPI_PREFIX}/docs", + redoc_url=f"/{settings.OCPI_PREFIX}/redoc", + openapi_url=f"/{settings.OCPI_PREFIX}/openapi.json", ) _app.add_middleware( @@ -67,54 +124,71 @@ def get_application( _app.include_router( versions_router, - prefix=f'/{settings.OCPI_PREFIX}', + prefix=f"/{settings.OCPI_PREFIX}", ) if http_push: _app.include_router( http_push_router, - prefix=f'/{settings.PUSH_PREFIX}', + prefix=f"/{settings.PUSH_PREFIX}", ) if websocket_push: _app.include_router( websocket_push_router, - prefix=f'/{settings.PUSH_PREFIX}', + prefix=f"/{settings.PUSH_PREFIX}", ) versions = [] - version_endpoints = {} + version_endpoints: dict[str, list] = {} + + for version in version_numbers: + mapped_version = ROUTERS.get(version) + if not mapped_version: + raise ValueError("Version isn't supported yet.") - if VersionNumber.v_2_2_1 in version_numbers: _app.include_router( - versions_v_2_2_1_router, - prefix=f'/{settings.OCPI_PREFIX}', + mapped_version["version_router"], + 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') + version=version, + url=URL( + f"{settings.PROTOCOL}://{settings.OCPI_HOST}/" + f"{settings.OCPI_PREFIX}/{version.value}/details" + ), ).dict(), ) - version_endpoints[VersionNumber.v_2_2_1] = [] + version_endpoints[version] = [] 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.value}', - tags=['CPO'] - ) - version_endpoints[VersionNumber.v_2_2_1] += ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.cpo] + for module in modules: + cpo_router = mapped_version["cpo_router"].get(module) + if cpo_router: + _app.include_router( + cpo_router, + prefix=f"/{settings.OCPI_PREFIX}/cpo/{version.value}", + tags=[f"CPO {version.value}"], + ) + endpoint = ENDPOINTS[version][RoleEnum.cpo].get(module) + if endpoint: + version_endpoints[version].append(endpoint) 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.value}', - tags=['EMSP'] - ) - version_endpoints[VersionNumber.v_2_2_1] += ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.emsp] + for module in modules: + emsp_router = mapped_version["emsp_router"].get(module) + if emsp_router: + _app.include_router( + emsp_router, + prefix=f"/{settings.OCPI_PREFIX}/emsp/{version.value}", + tags=[f"EMSP {version.value}"], + ) + endpoint = ENDPOINTS[version][RoleEnum.emsp].get(module) + if endpoint: + version_endpoints[version].append(endpoint) def override_get_crud(): return crud @@ -136,4 +210,14 @@ def override_get_endpoints(): _app.dependency_overrides[get_endpoints] = override_get_endpoints + def override_get_modules(): + return modules + + _app.dependency_overrides[get_modules] = override_get_modules() + + def override_get_authenticator(): + return authenticator + + _app.dependency_overrides[get_authenticator] = override_get_authenticator() + return _app diff --git a/py_ocpi/modules/__init__.py b/py_ocpi/modules/__init__.py index e69de29..bf42c8f 100644 --- a/py_ocpi/modules/__init__.py +++ b/py_ocpi/modules/__init__.py @@ -0,0 +1,3 @@ +from py_ocpi.modules.versions.main import router +from py_ocpi.modules.versions.v_2_2_1.api import router as versions_v_2_2_1_router +from py_ocpi.modules.versions.v_2_1_1.api import router as versions_v_2_1_1_router diff --git a/py_ocpi/modules/cdrs/v_2_1_1/api/__init__.py b/py_ocpi/modules/cdrs/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/cdrs/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/cdrs/v_2_1_1/api/cpo.py b/py_ocpi/modules/cdrs/v_2_1_1/api/cpo.py new file mode 100644 index 0000000..e2eb691 --- /dev/null +++ b/py_ocpi/modules/cdrs/v_2_1_1/api/cpo.py @@ -0,0 +1,65 @@ +from fastapi import APIRouter, Depends, Response, Request + +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core import status +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.utils import get_auth_token, get_list + +router = APIRouter( + prefix="/cdrs", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get("/", response_model=OCPIResponse) +async def get_cdrs( + response: Response, + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get CDRs. + + Retrieves a list of Charge Detail Records (CDRs) based on the specified + filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return CDRs that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return CDRs that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of CDRs. + """ + logger.info("Received request to get cdrs.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data_list = await get_list( + response, + filters, + ModuleID.cdrs, + RoleEnum.cpo, + VersionNumber.v_2_1_1, + crud, + auth_token=auth_token, + ) + + cdrs = [] + for data in data_list: + cdrs.append(adapter.cdr_adapter(data, VersionNumber.v_2_1_1).dict()) + logger.debug(f"Amount of cdrs in response: {len(cdrs)}") + return OCPIResponse( + data=cdrs, + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/cdrs/v_2_1_1/api/emsp.py b/py_ocpi/modules/cdrs/v_2_1_1/api/emsp.py new file mode 100644 index 0000000..2ab1a1c --- /dev/null +++ b/py_ocpi/modules/cdrs/v_2_1_1/api/emsp.py @@ -0,0 +1,105 @@ +from fastapi import APIRouter, Depends, Request, Response + +from py_ocpi.modules.cdrs.v_2_1_1.schemas import Cdr +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.utils import get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.data_types import CiString +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.config import settings +from py_ocpi.core.dependencies import get_crud, get_adapter + +router = APIRouter( + prefix="/cdrs", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get("/{cdr_id}", response_model=OCPIResponse) +async def get_cdr( + request: Request, + cdr_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get CDR by ID. + + Retrieves a Charge Detail Record (CDR) based on the specified ID. + + **Path parameters:** + - cdr_id (str): The ID of the CDR to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the CDR data. + + **Raises:** + NotFoundOCPIError: If the CDR is not found. + """ + logger.info(f"Received request to get cdr with id - `{cdr_id}`.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.cdrs, + RoleEnum.emsp, + cdr_id, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if data: + return OCPIResponse( + data=[adapter.cdr_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"CDR with id `{cdr_id}` was not found.") + raise NotFoundOCPIError + + +@router.post("/", response_model=OCPIResponse) +async def add_cdr( + request: Request, + response: Response, + cdr: Cdr, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add CDR. + + Creates a new Charge Detail Record (CDR) based on the specified parameters. + + **Request body:** + cdr (Cdr): The CDR object. + + **Returns:** + The OCPIResponse containing the created CDR data. + """ + logger.info("Received request to create cdr.") + logger.debug(f"CDR data to create - {cdr.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.create( + ModuleID.cdrs, + RoleEnum.emsp, + cdr.dict(), + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + cdr_data = adapter.cdr_adapter(data, VersionNumber.v_2_1_1) + cdr_url = ( + f"https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp" + f"/{VersionNumber.v_2_1_1}/{ModuleID.cdrs}/{cdr_data.id}" + ) + response.headers.append("Location", cdr_url) + + return OCPIResponse( + data=[cdr_data.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/cdrs/v_2_1_1/enums.py b/py_ocpi/modules/cdrs/v_2_1_1/enums.py new file mode 100644 index 0000000..ce96fb1 --- /dev/null +++ b/py_ocpi/modules/cdrs/v_2_1_1/enums.py @@ -0,0 +1,31 @@ +from enum import Enum + + +class AuthMethod(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md#41-authmethod-enum + """ + + # Authentication request from the eMSP + auth_request = "AUTH_REQUEST" + # Whitelist used to authenticate, no request done to the eMSP + whitelist = "WHITELIST" + + +class CdrDimensionType(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md#43-cdrdimensiontype-enum + """ + + # defined in kWh, default step_size is 1 Wh + energy = "ENERGY" + # flat fee, no unit + flat = "FLAT" + # defined in A (Ampere), Maximum current reached during charging session + max_current = "MAX_CURRENT" + # defined in A (Ampere), Minimum current used during charging session + min_current = "MIN_CURRENT" + # time not charging: defined in hours, default step_size is 1 second + parking_time = "PARKING_TIME" + # time charging: defined in hours, default step_size is 1 second + time = "TIME" diff --git a/py_ocpi/modules/cdrs/v_2_1_1/schemas.py b/py_ocpi/modules/cdrs/v_2_1_1/schemas.py new file mode 100644 index 0000000..d7bfca2 --- /dev/null +++ b/py_ocpi/modules/cdrs/v_2_1_1/schemas.py @@ -0,0 +1,52 @@ +from typing import List, Optional + +from pydantic import BaseModel +from py_ocpi.modules.cdrs.v_2_1_1.enums import ( + AuthMethod, + CdrDimensionType, +) + +from py_ocpi.core.data_types import CiString, Number, String, DateTime +from py_ocpi.modules.locations.v_2_1_1.schemas import Location +from py_ocpi.modules.tariffs.v_2_1_1.schemas import Tariff + + +class CdrDimension(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md#42-cdrdimension-class + """ + + type: CdrDimensionType + volume: Number + + +class ChargingPeriod(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md#44-chargingperiod-class + """ + + start_date_time: DateTime + dimensions: List[CdrDimension] + + +class Cdr(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md#31-cdr-object + """ + + id: CiString(36) # type: ignore + start_date_time: DateTime + end_date_time: DateTime + auth_id: String(36) # type: ignore + auth_method: AuthMethod + location: Location + meter_id: Optional[String(255)] # type: ignore + currency: String(3) # type: ignore + tariffs: List[Tariff] = [] + charging_periods: List[ChargingPeriod] + total_cost: Number + total_energy: Number + total_time: Number + total_parking_time: Optional[Number] + remark: Optional[String(255)] # type: ignore + last_updated: DateTime diff --git a/py_ocpi/modules/cdrs/v_2_2_1/api/cpo.py b/py_ocpi/modules/cdrs/v_2_2_1/api/cpo.py index 4fd66f8..c6c4b74 100644 --- a/py_ocpi/modules/cdrs/v_2_2_1/api/cpo.py +++ b/py_ocpi/modules/cdrs/v_2_2_1/api/cpo.py @@ -5,29 +5,60 @@ from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.enums import ModuleID, RoleEnum from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters router = APIRouter( - prefix='/cdrs', + prefix="/cdrs", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) @router.get("/", response_model=OCPIResponse) -async def get_cdrs(response: Response, - request: Request, - crud: Crud = Depends(get_crud), - adapter: Adapter = Depends(get_adapter), - filters: dict = Depends(pagination_filters)): +async def get_cdrs( + response: Response, + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get CDRs. + + Retrieves a list of Charge Detail Records (CDRs) based + on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return Locations that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return Locations that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of CDRs. + """ + logger.info("Received request to get cdrs.") auth_token = get_auth_token(request) - data_list = await get_list(response, filters, ModuleID.cdrs, RoleEnum.cpo, - VersionNumber.v_2_2_1, crud, auth_token=auth_token) + data_list = await get_list( + response, + filters, + ModuleID.cdrs, + RoleEnum.cpo, + VersionNumber.v_2_2_1, + crud, + auth_token=auth_token, + ) cdrs = [] for data in data_list: cdrs.append(adapter.cdr_adapter(data).dict()) + logger.debug(f"Amount of cdrs in response: {len(cdrs)}") return OCPIResponse( data=cdrs, **status.OCPI_1000_GENERIC_SUCESS_CODE, diff --git a/py_ocpi/modules/cdrs/v_2_2_1/api/emsp.py b/py_ocpi/modules/cdrs/v_2_2_1/api/emsp.py index ee77d9e..2ffe308 100644 --- a/py_ocpi/modules/cdrs/v_2_2_1/api/emsp.py +++ b/py_ocpi/modules/cdrs/v_2_2_1/api/emsp.py @@ -6,42 +6,98 @@ from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError from py_ocpi.core.config import settings from py_ocpi.core.dependencies import get_crud, get_adapter router = APIRouter( - prefix='/cdrs', + prefix="/cdrs", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) @router.get("/{cdr_id}", response_model=OCPIResponse) -async def get_cdr(request: Request, cdr_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def get_cdr( + request: Request, + cdr_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get CDR by ID. + + Retrieves a Charge Detail Record (CDR) based on the specified ID. + + **Path parameters:** + - cdr_id (str): The ID of the CDR to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the CDR data. + + **Raises:** + NotFoundOCPIError: If the CDR is not found. + """ + logger.info(f"Received request to get cdr with id - `{cdr_id}`.") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.cdrs, RoleEnum.emsp, cdr_id, auth_token=auth_token, - version=VersionNumber.v_2_2_1) - return OCPIResponse( - data=[adapter.cdr_adapter(data, VersionNumber.v_2_2_1).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + data = await crud.get( + ModuleID.cdrs, + RoleEnum.emsp, + cdr_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, ) + if data: + return OCPIResponse( + data=[adapter.cdr_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"CDR with id `{cdr_id}` was not found.") + raise NotFoundOCPIError @router.post("/", response_model=OCPIResponse) -async def add_cdr(request: Request, response: Response, cdr: Cdr, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def add_cdr( + request: Request, + response: Response, + cdr: Cdr, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add CDR. + + Creates a new Charge Detail Record (CDR) based on the specified parameters. + + **Request body:** + cdr (Cdr): The CDR object. + + **Returns:** + The OCPIResponse containing the created CDR data. + """ + logger.info("Received request to create cdr.") + logger.debug(f"CDR data to create - {cdr.dict()}") auth_token = get_auth_token(request) - data = await crud.create(ModuleID.cdrs, RoleEnum.emsp, cdr.dict(), - auth_token=auth_token, version=VersionNumber.v_2_2_1) + data = await crud.create( + ModuleID.cdrs, + RoleEnum.emsp, + cdr.dict(), + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) cdr_data = adapter.cdr_adapter(data) - cdr_url = (f'https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp' - f'/{VersionNumber.v_2_2_1}/{ModuleID.cdrs}/{cdr_data.id}') - response.headers.append('Location', cdr_url) + cdr_url = ( + f"https://{settings.OCPI_HOST}/{settings.OCPI_PREFIX}/emsp" + f"/{VersionNumber.v_2_2_1}/{ModuleID.cdrs}/{cdr_data.id}" + ) + response.headers.append("Location", cdr_url) return OCPIResponse( data=[cdr_data.dict()], diff --git a/py_ocpi/modules/cdrs/v_2_2_1/enums.py b/py_ocpi/modules/cdrs/v_2_2_1/enums.py index aa76459..c677b8b 100644 --- a/py_ocpi/modules/cdrs/v_2_2_1/enums.py +++ b/py_ocpi/modules/cdrs/v_2_2_1/enums.py @@ -6,48 +6,60 @@ class AuthMethod(str, Enum): https://github.com/ocpi/ocpi/blob/2.2.1/mod_cdrs.asciidoc#141-authmethod-enum """ # Authentication request has been sent to the eMSP. - auth_request = 'AUTH_REQUEST' + auth_request = "AUTH_REQUEST" # Command like StartSession or ReserveNow used to start the Session, # the Token provided in the Command was used as authorization. - command = 'COMMAND' - # Whitelist used for authentication, no request to the eMSP has been performed. - whitelist = 'WHITELIST' + command = "COMMAND" + # Whitelist used for authentication, + # no request to the eMSP has been performed. + whitelist = "WHITELIST" class CdrDimensionType(str, Enum): """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_cdrs.asciidoc#143-cdrdimensiontype-enum """ - # Average charging current during this ChargingPeriod: defined in A (Ampere). + # Average charging current during this ChargingPeriod: defined in A + # (Ampere). # When negative, the current is flowing from the EV to the grid. - current = 'CURRENT' - # Total amount of energy (dis-)charged during this ChargingPeriod: defined in kWh. - # When negative, more energy was feed into the grid then charged into the EV. + current = "CURRENT" + # Total amount of energy (dis-)charged during this ChargingPeriod: + # defined in kWh. + # When negative, more energy was feed into the grid then + # charged into the EV. # Default step_size is 1. - energy = 'ENERGY' + energy = "ENERGY" # Total amount of energy feed back into the grid: defined in kWh. - energy_export = 'ENERGY_EXPORT' + energy_export = "ENERGY_EXPORT" # Total amount of energy charged, defined in kWh. - energy_import = 'ENERGY_IMPORT' - # Sum of the maximum current over all phases, reached during this ChargingPeriod: defined in A (Ampere). - max_current = 'MAX_CURRENT' - # Sum of the minimum current over all phases, reached during this ChargingPeriod, when negative, + energy_import = "ENERGY_IMPORT" + # Sum of the maximum current over all phases, reached during this + # ChargingPeriod: defined in A (Ampere). + max_current = "MAX_CURRENT" + # Sum of the minimum current over all phases, reached during this + # ChargingPeriod, when negative, # current has flowed from the EV to the grid. Defined in A (Ampere). - min_current = 'MIN_CURRENT' - # Maximum power reached during this ChargingPeriod: defined in kW (Kilowatt). - max_power = 'MAX_POWER' - # Minimum power reached during this ChargingPeriod: defined in kW (Kilowatt), + min_current = "MIN_CURRENT" + # Maximum power reached during this ChargingPeriod: defined in kW + # (Kilowatt). + max_power = "MAX_POWER" + # Minimum power reached during this ChargingPeriod: defined in kW + # (Kilowatt), # when negative, the power has flowed from the EV to the grid. - min_power = 'MIN_POWER' - # Time during this ChargingPeriod not charging: defined in hours, default step_size multiplier is 1 second. - parking_time = 'PARKING_TIME' + min_power = "MIN_POWER" + # Time during this ChargingPeriod not charging: defined in hours, + # default step_size multiplier is 1 second. + parking_time = "PARKING_TIME" # Average power during this ChargingPeriod: defined in kW (Kilowatt). # When negative, the power is flowing from the EV to the grid. - power = 'POWER' - # Time during this ChargingPeriod Charge Point has been reserved and not yet been in use for this customer: + power = "POWER" + # Time during this ChargingPeriod Charge Point has been reserved + # and not yet been in use for this customer: # defined in hours, default step_size multiplier is 1 second. - reservation_time = 'RESERVATION_TIME' - # Current state of charge of the EV, in percentage, values allowed: 0 to 100. See note below. - state_of_change = 'STATE_OF_CHARGE' - # Time charging during this ChargingPeriod: defined in hours, default step_size multiplier is 1 second. - time = 'TIME' + reservation_time = "RESERVATION_TIME" + # Current state of charge of the EV, in percentage, + # values allowed: 0 to 100. See note below. + state_of_change = "STATE_OF_CHARGE" + # Time charging during this ChargingPeriod: defined in hours, + # default step_size multiplier is 1 second. + time = "TIME" 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 704f171..8bf376a 100644 --- a/py_ocpi/modules/cdrs/v_2_2_1/schemas.py +++ b/py_ocpi/modules/cdrs/v_2_2_1/schemas.py @@ -7,7 +7,11 @@ from py_ocpi.modules.tokens.v_2_2_1.enums import TokenType from py_ocpi.modules.tariffs.v_2_2_1.schemas import Tariff from py_ocpi.modules.locations.v_2_2_1.schemas import GeoLocation -from py_ocpi.modules.locations.v_2_2_1.enums import ConnectorFormat, ConnectorType, PowerType +from py_ocpi.modules.locations.v_2_2_1.enums import ( + ConnectorFormat, + ConnectorType, + PowerType, +) class SignedValue(BaseModel): diff --git a/py_ocpi/modules/chargingprofiles/__init__.py b/py_ocpi/modules/chargingprofiles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py_ocpi/modules/chargingprofiles/v_2_2_1/api/__init__.py b/py_ocpi/modules/chargingprofiles/v_2_2_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/chargingprofiles/v_2_2_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/chargingprofiles/v_2_2_1/api/cpo.py b/py_ocpi/modules/chargingprofiles/v_2_2_1/api/cpo.py new file mode 100644 index 0000000..608d158 --- /dev/null +++ b/py_ocpi/modules/chargingprofiles/v_2_2_1/api/cpo.py @@ -0,0 +1,309 @@ +from fastapi import APIRouter, BackgroundTasks, Depends, Request + +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.utils import get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.data_types import CiString, URL +from py_ocpi.core.enums import ModuleID, RoleEnum, Action +from py_ocpi.core.dependencies import get_crud, get_adapter + +from py_ocpi.modules.chargingprofiles.v_2_2_1.background_tasks import ( + send_get_chargingprofile, + send_delete_chargingprofile, + send_update_chargingprofile, +) +from py_ocpi.modules.chargingprofiles.v_2_2_1.schemas import ( + ChargingProfileResponse, + ChargingProfileResponseType, + SetChargingProfile, +) + +router = APIRouter( + prefix="/chargingprofiles", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) + + +@router.get("/{session_id}", response_model=OCPIResponse) +async def get_chargingprofile( + request: Request, + session_id: CiString(36), # type: ignore + duration: int, + response_url: URL, + background_tasks: BackgroundTasks, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Charging Profile. + + Retrieves the charging profile for a specific session. + + **Path parameters:** + - session_id (str): The ID of the charging session. + + **Query parameters:** + - duration (int): The requested duration for the charging profile. + - response_url (URL): The URL to send the charging profile response. + + **Returns:** + The OCPIResponse containing the charging profile response. + + **Raises:** + - NotFoundOCPIError: If the specified charging session is not found. + """ + logger.info(f"Received request to get charging profile with session_id - `{session_id}`.") + auth_token = get_auth_token(request) + + session = await crud.get( + ModuleID.sessions, + RoleEnum.cpo, + session_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + if session: + charging_profile_response = await crud.do( + ModuleID.charging_profile, + RoleEnum.cpo, + Action.send_get_chargingprofile, + session=session, + duration=duration, + response_url=response_url, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + if charging_profile_response: + if ( + charging_profile_response["result"] + == ChargingProfileResponseType.accepted + ): + background_tasks.add_task( + send_get_chargingprofile, + session_id=session_id, + duration=duration, + response_url=response_url, + auth_token=auth_token, + crud=crud, + adapter=adapter, + ) + return OCPIResponse( + data=[ + adapter.charging_profile_response_adapter( + charging_profile_response + ).dict() + ], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + logger.debug( + "Sent get charging profile action returned without result." + ) + charging_profile_response = ChargingProfileResponse( + result=ChargingProfileResponseType.rejected, timeout=0 + ) + return OCPIResponse( + data=[charging_profile_response.dict()], + **status.OCPI_3000_GENERIC_SERVER_ERROR, + ) + + logger.info(f"Session with id `{session_id}` was not found.") + charging_profile_response = ChargingProfileResponse( + result=ChargingProfileResponseType.rejected, timeout=0 + ) + return OCPIResponse( + data=[charging_profile_response.dict()], + **status.OCPI_2000_GENERIC_CLIENT_ERROR, + ) + + +@router.put("/{session_id}", response_model=OCPIResponse) +async def add_or_update_chargingprofile( + request: Request, + session_id: CiString(36), # type: ignore + charging_profile: SetChargingProfile, + background_tasks: BackgroundTasks, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Charging Profile. + + Adds or updates the charging profile for a specific session. + + **Path parameters:** + - session_id (str): The ID of the charging session. + + **Request body:** + - charging_profile (SetChargingProfile): The charging profile data. + + **Returns:** + The OCPIResponse containing the charging profile response. + + **Raises:** + - NotFoundOCPIError: If the specified charging session is not found. + """ + logger.info(f"Received request to get charging profile with session_id - `{session_id}`.") + logger.debug(f"Set charging profile data - `{charging_profile.dict()}`") + auth_token = get_auth_token(request) + + session = await crud.get( + ModuleID.sessions, + RoleEnum.cpo, + session_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + if session: + charging_profile_response = await crud.do( + ModuleID.charging_profile, + RoleEnum.cpo, + Action.send_update_charging_profile, + charging_profile.dict(), + session=session, + response_url=charging_profile.response_url, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + if charging_profile_response: + if ( + charging_profile_response["result"] + == ChargingProfileResponseType.accepted + ): + background_tasks.add_task( + send_update_chargingprofile, + charging_profile=charging_profile, + session_id=session_id, + response_url=charging_profile.response_url, + auth_token=auth_token, + crud=crud, + adapter=adapter, + ) + return OCPIResponse( + data=[ + adapter.charging_profile_response_adapter( + charging_profile_response + ).dict() + ], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + logger.debug( + "Sent update charging profile action returned without result." + ) + charging_profile_response = ChargingProfileResponse( + result=ChargingProfileResponseType.rejected, timeout=0 + ) + return OCPIResponse( + data=[charging_profile_response.dict()], + **status.OCPI_3000_GENERIC_SERVER_ERROR, + ) + + logger.info(f"Session with id `{session_id}` was not found.") + charging_profile_response = ChargingProfileResponse( + result=ChargingProfileResponseType.rejected, timeout=0 + ) + return OCPIResponse( + data=[charging_profile_response.dict()], + **status.OCPI_2000_GENERIC_CLIENT_ERROR, + ) + + +@router.delete("/{session_id}", response_model=OCPIResponse) +async def delete_chargingprofile( + request: Request, + session_id: CiString(36), # type: ignore + response_url: URL, + background_tasks: BackgroundTasks, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Delete Charging Profile. + + Deletes the charging profile for a specific session. + + **Path parameters:** + - session_id (str): The ID of the charging session. + + **Query parameters:** + - response_url (URL): The URL to send the response to. + + **Returns:** + The OCPIResponse containing the charging profile response. + + **Raises:** + - NotFoundOCPIError: If the specified charging session is not found. + """ + logger.info(f"Received request to get charging profile with session_id - `{session_id}`.") + auth_token = get_auth_token(request) + + session = await crud.get( + ModuleID.sessions, + RoleEnum.cpo, + session_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + if session: + charging_profile_response = await crud.do( + ModuleID.charging_profile, + RoleEnum.cpo, + Action.send_delete_chargingprofile, + session=session, + response_url=response_url, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + if charging_profile_response: + if ( + charging_profile_response["result"] + == ChargingProfileResponseType.accepted + ): + background_tasks.add_task( + send_delete_chargingprofile, + session_id=session_id, + response_url=response_url, + auth_token=auth_token, + crud=crud, + adapter=adapter, + ) + return OCPIResponse( + data=[ + adapter.charging_profile_response_adapter( + charging_profile_response + ).dict() + ], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + logger.debug( + "Sent delete charging profile action returned without result." + ) + charging_profile_response = ChargingProfileResponse( + result=ChargingProfileResponseType.rejected, timeout=0 + ) + return OCPIResponse( + data=[charging_profile_response.dict()], + **status.OCPI_3000_GENERIC_SERVER_ERROR, + ) + + logger.info(f"Session with id `{session_id}` was not found.") + charging_profile_response = ChargingProfileResponse( + result=ChargingProfileResponseType.rejected, timeout=0 + ) + return OCPIResponse( + data=[charging_profile_response.dict()], + **status.OCPI_2000_GENERIC_CLIENT_ERROR, + ) diff --git a/py_ocpi/modules/chargingprofiles/v_2_2_1/api/emsp.py b/py_ocpi/modules/chargingprofiles/v_2_2_1/api/emsp.py new file mode 100644 index 0000000..29080d0 --- /dev/null +++ b/py_ocpi/modules/chargingprofiles/v_2_2_1/api/emsp.py @@ -0,0 +1,106 @@ +from fastapi import APIRouter, Depends, Request + +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.utils import get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.dependencies import get_crud, get_adapter + +from py_ocpi.modules.chargingprofiles.v_2_2_1.schemas import ( + ActiveChargingProfile, +) + +router = APIRouter( + prefix="/chargingprofiles", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) + + +@router.post("/", response_model=OCPIResponse) +async def receive_chargingprofile_command( + request: Request, + data: dict, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Receive Charging Profile Command. + + Receives and processes the charging profile command. + + **Parameters:** + - data (dict): The charging profile command data. + + **Returns:** + The OCPIResponse indicating the success of the operation. + """ + logger.info("Received charging profile result.") + logger.debug(f"Chargingprofile result data - {data}") + auth_token = get_auth_token(request) + query_params = request.query_params + logger.debug(f"Request query_params - {query_params}") + + await crud.create( + ModuleID.charging_profile, + RoleEnum.emsp, + data, + query_params=query_params, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.put("/{session_id}", response_model=OCPIResponse) +async def add_or_update_chargingprofile( + request: Request, + session_id: str, + active_charging_profile: ActiveChargingProfile, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Charging Profile. + + Adds or updates the active charging profile for a specific session. + + **Parameters:** + - session_id (str): The ID of the charging session. + + **Request body:** + - active_charging_profile (ActiveChargingProfile): The data + of the active charging profile. + + **Returns:** + The OCPIResponse indicating the success of the operation. + """ + logger.info( + "Received request to add or update charging profile " + f"with session_id - `{session_id}`." + ) + logger.debug(f"Active chargingprofile result data - {active_charging_profile.dict()}") + auth_token = get_auth_token(request) + + await crud.update( + ModuleID.charging_profile, + RoleEnum.emsp, + active_charging_profile.dict(), + 0, + session_id=session_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/chargingprofiles/v_2_2_1/background_tasks.py b/py_ocpi/modules/chargingprofiles/v_2_2_1/background_tasks.py new file mode 100644 index 0000000..fd8a951 --- /dev/null +++ b/py_ocpi/modules/chargingprofiles/v_2_2_1/background_tasks.py @@ -0,0 +1,208 @@ +from asyncio import sleep + +import httpx +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.utils import encode_string_base64 +from py_ocpi.core.config import settings +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.data_types import CiString, URL +from py_ocpi.core.enums import ModuleID, RoleEnum, Action + +from py_ocpi.modules.chargingprofiles.v_2_2_1.schemas import ( + ChargingProfileResult, + ChargingProfileResultType, + SetChargingProfile, +) + + +async def send_get_chargingprofile( + session_id: CiString(36), # type: ignore + duration: int, + response_url: URL, + auth_token: str, + crud: Crud, + adapter: Adapter, +): + logger.info("Received command to send get chargingprofile request.") + client_auth_token = await crud.do( + ModuleID.charging_profile, + RoleEnum.cpo, + Action.get_client_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + active_charging_profile_result = None + for _ in range(30 * settings.GET_ACTIVE_PROFILE_AWAIT_TIME): + # since charging profile has no id, 0 is used for id parameter of crud.get + active_charging_profile_result = await crud.get( + ModuleID.hub_client_info, + RoleEnum.cpo, + 0, + session_id=session_id, + duration=duration, + response_url=response_url, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + if active_charging_profile_result: + logger.debug(f"Active charging profile result from Charge Point - {active_charging_profile_result}") + break + await sleep(2) + + if not active_charging_profile_result: + logger.debug( + "Active charging profile result from Charge Point " + "didn't arrive in time." + ) + active_charging_profile_result = ChargingProfileResult( + result=ChargingProfileResultType.rejected + ) + else: + active_charging_profile_result = ( + adapter.active_charging_profile_result_adapter( + active_charging_profile_result, VersionNumber.v_2_2_1 + ) + ) + + async with httpx.AsyncClient() as client: # nosec + authorization_token = f"Token {encode_string_base64(client_auth_token)}" + logger.info(f"Send request with active charging profile result: {response_url}") + res = await client.post( + response_url, + json=active_charging_profile_result.dict(), + headers={"authorization": authorization_token}, + ) + logger.info( + "POST active chargingprofile result data after receiving result " + f"from Charge Point status_code: {res.status_code}" + ) + + +async def send_update_chargingprofile( + charging_profile: SetChargingProfile, + session_id: CiString(36), # type: ignore + response_url: URL, + auth_token: str, + crud: Crud, + adapter: Adapter, +): + logger.info("Received command to send update chargingprofile request.") + client_auth_token = await crud.do( + ModuleID.charging_profile, + RoleEnum.cpo, + Action.get_client_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + charging_profile_result = None + for _ in range(30 * settings.GET_ACTIVE_PROFILE_AWAIT_TIME): + # since charging profile has no id, 0 is used for id parameter of crud.get + charging_profile_result = await crud.get( + ModuleID.hub_client_info, + RoleEnum.cpo, + 0, + session_id=session_id, + response_url=response_url, + charging_profile=charging_profile, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + if not charging_profile_result: + logger.debug(f"Charging profile result from Charge Point - {charging_profile_result}") + break + await sleep(2) + + if not charging_profile_result: + logger.debug( + "Charging profile result from Charge Point " + "didn't arrive in time." + ) + charging_profile_result = ChargingProfileResult( + result=ChargingProfileResultType.rejected + ) + else: + charging_profile_result = ( + adapter.active_charging_profile_result_adapter( + charging_profile_result, VersionNumber.v_2_2_1 + ) + ) + + async with httpx.AsyncClient() as client: # nosec + authorization_token = f"Token {encode_string_base64(client_auth_token)}" + logger.info( + f"Send request with charging profile result: {response_url}" + ) + res = await client.post( + response_url, + json=charging_profile_result.dict(), + headers={"authorization": authorization_token}, + ) + logger.info( + "POST charging profile result data after receiving result " + f"from Charge Point status_code: {res.status_code}" + ) + + +async def send_delete_chargingprofile( + session_id: CiString(36), # type: ignore + response_url: URL, + auth_token: str, + crud: Crud, + adapter: Adapter, +): + logger.info("Received command to send delete chargingprofile request.") + client_auth_token = await crud.do( + ModuleID.charging_profile, + RoleEnum.cpo, + Action.get_client_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + clear_profile_result = None + for _ in range(30 * settings.GET_ACTIVE_PROFILE_AWAIT_TIME): + # since charging profile has no id, 0 is used for id parameter of crud.get + clear_profile_result = await crud.get( + ModuleID.hub_client_info, + RoleEnum.cpo, + 0, + session_id=session_id, + response_url=response_url, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + if not clear_profile_result: + logger.debug( + f"Clear profile result from Charge Point - {clear_profile_result}" + ) + break + await sleep(2) + + if clear_profile_result: + logger.debug( + "Clear profile result from Charge Point didn't arrive in time." + ) + clear_profile_result = ChargingProfileResult( + result=ChargingProfileResultType.rejected + ) + else: + clear_profile_result = ChargingProfileResult( + result=ChargingProfileResultType.accepted + ) + + async with httpx.AsyncClient() as client: # nosec + authorization_token = f"Token {encode_string_base64(client_auth_token)}" + logger.info(f"Send request with clear profile result: {response_url}") + res = await client.post( + response_url, + json=clear_profile_result.dict(), + headers={"authorization": authorization_token}, + ) + logger.info( + "POST clear profile result data after receiving result " + f"from Charge Point status_code: {res.status_code}" + ) diff --git a/py_ocpi/modules/chargingprofiles/v_2_2_1/enums.py b/py_ocpi/modules/chargingprofiles/v_2_2_1/enums.py new file mode 100644 index 0000000..d63afab --- /dev/null +++ b/py_ocpi/modules/chargingprofiles/v_2_2_1/enums.py @@ -0,0 +1,29 @@ +from enum import Enum + + +class ChargingProfileResultType(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#166-chargingprofileresulttype-enum + """ + accepted = "ACCEPTED" + rejected = "REJECTED" + unknown = "UNKNOWN" + + +class ChargingProfileResponseType(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#165-chargingprofileresponsetype-enum + """ + accepted = "ACCEPTED" + not_supported = "NOT_SUPPORTED" + rejected = "REJECTED" + too_often = "TOO_OFTEN" + unknown_session = "UNKNOWN_SESSION" + + +class ChargingRateUnit(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#162-chargingrateunit-enum + """ + watts = "W" + amperes = "A" diff --git a/py_ocpi/modules/chargingprofiles/v_2_2_1/schemas.py b/py_ocpi/modules/chargingprofiles/v_2_2_1/schemas.py new file mode 100644 index 0000000..8b048ce --- /dev/null +++ b/py_ocpi/modules/chargingprofiles/v_2_2_1/schemas.py @@ -0,0 +1,82 @@ +from typing import Optional +from pydantic import BaseModel + +from py_ocpi.core.data_types import DateTime, Number, URL +from py_ocpi.modules.chargingprofiles.v_2_2_1.enums import ( + ChargingProfileResponseType, + ChargingProfileResultType, + ChargingRateUnit, +) + + +class ChargingProfilePeriod(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#164-chargingprofileperiod-class + """ + + start_period: int + limit: Number + + +class ChargingProfile(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#163-chargingprofile-class + """ + + start_date_time: Optional[DateTime] + duration: Optional[int] + charging_rate_unit: ChargingRateUnit + min_charge_rate: Number + charging_profile_period: ChargingProfilePeriod + + +class ActiveChargingProfile(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#161-activechargingprofile-class + """ + + start_date_time: DateTime + charging_profile: ChargingProfile + + +class ChargingProfileResponse(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#151-chargingprofileresponse-object + """ + + result: ChargingProfileResponseType + timeout: int + + +class ActiveChargingProfileResult(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#152-activechargingprofileresult-object + """ + + result: ChargingProfileResultType + profile: Optional[ActiveChargingProfile] + + +class ChargingProfileResult(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#153-chargingprofileresult-object + """ + + result: ChargingProfileResultType + + +class ClearProfileResult(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#154-clearprofileresult-object + """ + + result: ChargingProfileResultType + + +class SetChargingProfile(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_charging_profiles.asciidoc#155-setchargingprofile-object + """ + + charging_profile: ChargingProfile + response_url: URL diff --git a/py_ocpi/modules/commands/v_2_1_1/api/__init__.py b/py_ocpi/modules/commands/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/commands/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/commands/v_2_1_1/api/cpo.py b/py_ocpi/modules/commands/v_2_1_1/api/cpo.py new file mode 100644 index 0000000..05219aa --- /dev/null +++ b/py_ocpi/modules/commands/v_2_1_1/api/cpo.py @@ -0,0 +1,221 @@ +from typing import Union +from asyncio import sleep + +from fastapi import ( + APIRouter, + BackgroundTasks, + Depends, + Request, + status as fastapistatus, +) +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from pydantic import ValidationError +import httpx + +from py_ocpi.core.dependencies import get_crud, get_adapter +from py_ocpi.core.enums import ModuleID, RoleEnum, Action +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core import status +from py_ocpi.core.config import settings +from py_ocpi.core.utils import get_auth_token +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.commands.v_2_1_1.enums import CommandType +from py_ocpi.modules.commands.v_2_1_1.schemas import ( + ReserveNow, + StartSession, + StopSession, + UnlockConnector, + CommandResponse, + CommandResponseType, +) + +router = APIRouter( + prefix="/commands", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +async def apply_pydantic_schema(command: str, data: dict): + if command == CommandType.reserve_now: + data = ReserveNow(**data) # type: ignore + elif command == CommandType.start_session: + data = StartSession(**data) # type: ignore + elif command == CommandType.stop_session: + data = StopSession(**data) # type: ignore + elif command == CommandType.unlock_connector: + data = UnlockConnector(**data) # type: ignore + else: + raise NotImplementedError + return data + + +async def send_command_result( + command_data: Union[StartSession, StopSession, ReserveNow, UnlockConnector], + command: CommandType, + auth_token: str, + crud: Crud, + adapter: Adapter, +): + client_auth_token = await crud.do( + ModuleID.commands, + RoleEnum.cpo, + Action.get_client_token, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + command_result = None + for _ in range(30 * settings.COMMAND_AWAIT_TIME): + # since command has no id, 0 is used for id parameter of crud.get + command_result = await crud.get( + ModuleID.commands, + RoleEnum.cpo, + 0, + command_data=command_data, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + command=command, + ) + if command_result: + logger.info( + f"Command result from Charge Point - {command_result}" + ) + break + await sleep(2) + + if not command_result: + logger.info("Command result from Charge Point didn't arrive in time.") + command_response = CommandResponse(result=CommandResponseType.timeout) + else: + command_response = adapter.command_response_adapter( + command_result, VersionNumber.v_2_1_1 + ) + + async with httpx.AsyncClient() as client: # nosec + authorization_token = f"Token {client_auth_token}" + logger.info( + f"Send request with command result: {command_data.response_url}" + ) + res = await client.post( + command_data.response_url, + json=command_response.dict(), + headers={"authorization": authorization_token}, + ) + logger.info( + "POST command data after receiving result from Charge Point" + f" status_code: {res.status_code}" + ) + + +@router.post("/{command}", response_model=OCPIResponse) +async def receive_command( + request: Request, + command: CommandType, + data: dict, + background_tasks: BackgroundTasks, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Receive Command. + + Processes and handles incoming commands. + + **Path parameters:** + - command (CommandType): The type of the command. + + **Request body:** + data (dict): The data associated with the command. + + **Returns:** + The OCPIResponse indicating the success or failure of the command. + + **Raises:** + - HTTPException: If there is a validation error or + if the command action returns without a result. + - NotFoundOCPIError: If the associated location is not found. + """ + logger.info(f"Received command - `{command}`.") + logger.debug(f"Command data - {data}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + try: + command_data = await apply_pydantic_schema(command, data) + except ValidationError as exc: + logger.debug("ValidationError on applying pydantic schema to command") + return JSONResponse( + status_code=fastapistatus.HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": jsonable_encoder(exc.errors())}, + ) + except NotImplementedError: + logger.debug( + "NotImplementedError on applying pydantic schema to command" + ) + return JSONResponse( + status_code=fastapistatus.HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": "Not implemented"}, + ) + + try: + if hasattr(command_data, "location_id"): + location = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + command_data.location_id, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if not location: + raise NotFoundOCPIError + + command_response = await crud.do( + ModuleID.commands, + RoleEnum.cpo, + Action.send_command, + command_data.dict(), + command=command, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if command_response: + if command_response["result"] == CommandResponseType.accepted: + background_tasks.add_task( + send_command_result, + command_data=command_data, + command=command, + auth_token=auth_token, + crud=crud, + adapter=adapter, + ) + return OCPIResponse( + data=[ + adapter.command_response_adapter( + command_response, VersionNumber.v_2_1_1 + ).dict() + ], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + logger.debug("Send command action returned without result.") + command_response = CommandResponse(result=CommandResponseType.rejected) + return OCPIResponse( + data=[command_response.dict()], + **status.OCPI_3000_GENERIC_SERVER_ERROR, + ) + + # when the location is not found + except NotFoundOCPIError: + logger.info( + f"Location with id `{command_data.location_id}` was not found." + ) + command_response = CommandResponse(result=CommandResponseType.rejected) + return OCPIResponse( + data=[command_response.dict()], + **status.OCPI_2003_UNKNOWN_LOCATION, + ) diff --git a/py_ocpi/modules/commands/v_2_1_1/api/emsp.py b/py_ocpi/modules/commands/v_2_1_1/api/emsp.py new file mode 100644 index 0000000..0be1130 --- /dev/null +++ b/py_ocpi/modules/commands/v_2_1_1/api/emsp.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, Depends, Request + +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.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core import status +from py_ocpi.core.utils import get_auth_token +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.commands.v_2_1_1.schemas import CommandResponse + +router = APIRouter( + prefix="/commands", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.post("/{uid}", response_model=OCPIResponse) +async def receive_command_result( + request: Request, + uid: str, + command_response: CommandResponse, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Receive Command Result. + + Processes and handles incoming command results. + + **Path parameters:** + - uid (str): The unique identifier associated with the command. + + **Request body:** + command_response (CommandResponse): The response data associated + with the command. + + **Returns:** + The OCPIResponse indicating the success or failure of processing + the command result. + """ + logger.info(f"Received command result with uid - `{uid}`.") + logger.debug(f"Command response data - {command_response.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + await crud.update( + ModuleID.commands, + RoleEnum.emsp, + command_response.dict(), + uid, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/commands/v_2_1_1/enums.py b/py_ocpi/modules/commands/v_2_1_1/enums.py new file mode 100644 index 0000000..632609f --- /dev/null +++ b/py_ocpi/modules/commands/v_2_1_1/enums.py @@ -0,0 +1,38 @@ +from enum import Enum + + +class CommandResponseType(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_commands.md#41-commandresponsetype-enum + """ + + # The requested command is not supported by this CPO, + # Charge Point, EVSE etc. + not_supported = "NOT_SUPPORTED" + # Command request rejected by the CPO or Charge Point. + rejected = "REJECTED" + # Command request accepted by the CPO or Charge Point. + accepted = "ACCEPTED" + # Command request timeout, no response received from + # the Charge Point in a reasonable time. + timeout = "TIMEOUT" + # The Session in the requested command is not known by this CPO. + unknown_session = "UNKNOWN_SESSION" + + +class CommandType(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_commands.md#42-commandtype-enum + """ + + # Request the Charge Point to reserve a (specific) EVSE + # for a Token for a certain time, starting now. + reserve_now = "RESERVE_NOW" + # Request the Charge Point to start a transaction on + # the given EVSE/Connector. + start_session = "START_SESSION" + # Request the Charge Point to stop an ongoing session. + stop_session = "STOP_SESSION" + # Request the Charge Point to unlock the connector (if applicable). + # This functionality is for help desk operators only! + unlock_connector = "UNLOCK_CONNECTOR" diff --git a/py_ocpi/modules/commands/v_2_1_1/schemas.py b/py_ocpi/modules/commands/v_2_1_1/schemas.py new file mode 100644 index 0000000..b8beb8a --- /dev/null +++ b/py_ocpi/modules/commands/v_2_1_1/schemas.py @@ -0,0 +1,58 @@ +from typing import Optional +from pydantic import BaseModel + +from py_ocpi.core.data_types import String, URL, DateTime +from py_ocpi.modules.commands.v_2_1_1.enums import CommandResponseType +from py_ocpi.modules.tokens.v_2_1_1.schemas import Token + + +class CommandResponse(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_commands.md#31-commandresponse-object + """ + + result: CommandResponseType + + +class ReserveNow(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_commands.md#32-reservenow-object + """ + + response_url: URL + token: Token + expiry_date: DateTime + reservation_id: int + location_id: String(36) # type: ignore + evse_uid: Optional[String(39)] # type: ignore + + +class StartSession(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_commands.md#33-startsession-object + """ + + response_url: URL + token: Token + location_id: String(39) # type: ignore + evse_uid: Optional[String(39)] # type: ignore + + +class StopSession(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_commands.md#34-stopsession-object + """ + + response_url: URL + session_id: String(36) # type: ignore + + +class UnlockConnector(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_commands.md#35-unlockconnector-object + """ + + response_url: URL + location_id: String(39) # type: ignore + evse_uid: String(39) # type: ignore + connector_id: String(39) # type: ignore 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 310e787..b54808b 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 @@ -1,6 +1,13 @@ from asyncio import sleep - -from fastapi import APIRouter, BackgroundTasks, Depends, Request, status as fastapistatus +from typing import Union + +from fastapi import ( + APIRouter, + BackgroundTasks, + Depends, + Request, + status as fastapistatus, +) from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse from pydantic import ValidationError @@ -8,23 +15,36 @@ from py_ocpi.core.dependencies import get_crud, get_adapter from py_ocpi.core.enums import ModuleID, RoleEnum, Action +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.exceptions import NotFoundOCPIError 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.config import logger from py_ocpi.core import status +from py_ocpi.core.config import settings 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 ( - CancelReservation, ReserveNow, StartSession, - StopSession, UnlockConnector, CommandResult, - CommandResultType, CommandResponse, CommandResponseType + CancelReservation, + ReserveNow, + StartSession, + StopSession, + UnlockConnector, + CommandResult, + CommandResultType, + CommandResponse, + CommandResponseType, ) router = APIRouter( - prefix='/commands', + prefix="/commands", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) +UnionDataType = Union[ + StartSession, StopSession, ReserveNow, UnlockConnector, CancelReservation +] async def apply_pydantic_schema(command: str, data: dict): @@ -41,62 +61,159 @@ async def apply_pydantic_schema(command: str, data: dict): return data -async def send_command_result(command_data: dict, command: CommandType, auth_token: str, crud: Crud, adapter: Adapter): - client_auth_token = await crud.do(ModuleID.commands, RoleEnum.cpo, Action.get_client_token, - auth_token=auth_token, version=VersionNumber.v_2_2_1) - - for _ in range(150): # check for 5 mins +async def send_command_result( + command_data: UnionDataType, + command: CommandType, + auth_token: str, + crud: Crud, + adapter: Adapter, +): + client_auth_token = await crud.do( + ModuleID.commands, + RoleEnum.cpo, + Action.get_client_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + + command_result = None + for _ in range(30 * settings.COMMAND_AWAIT_TIME): # since command has no id, 0 is used for id parameter of crud.get - command_result = await crud.get(ModuleID.commands, RoleEnum.cpo, 0, - auth_token=auth_token, version=VersionNumber.v_2_2_1, command=command, - command_data=command_data) + command_result = await crud.get( + ModuleID.commands, + RoleEnum.cpo, + 0, + command_data=command_data, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + command=command, + ) if command_result: + logger.info( + f"Command result from Charge Point - {command_result}" + ) break await sleep(2) if not command_result: + logger.info("Command result from Charge Point didn't arrive in time.") command_result = CommandResult(result=CommandResultType.failed) else: - command_result = adapter.command_result_adapter(command_result, VersionNumber.v_2_2_1) + command_result = adapter.command_result_adapter( + command_result, VersionNumber.v_2_2_1 + ) - async with httpx.AsyncClient() as client: - authorization_token = f'Token {encode_string_base64(client_auth_token)}' - await client.post(command_data.response_url, json=command_result.dict(), - headers={'authorization': authorization_token}) + async with httpx.AsyncClient() as client: # nosec + authorization_token = f"Token {encode_string_base64(client_auth_token)}" + logger.info( + f"Send request with command result: {command_data.response_url}" + ) + res = await client.post( + command_data.response_url, + json=command_result.dict(), + headers={"authorization": authorization_token}, + ) + logger.info( + "POST command data after receiving result from Charge Point" + f" status_code: {res.status_code}" + ) @router.post("/{command}", response_model=OCPIResponse) -async def receive_command(request: Request, command: CommandType, data: dict, background_tasks: BackgroundTasks, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def receive_command( + request: Request, + command: CommandType, + data: dict, + background_tasks: BackgroundTasks, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Receive Command. + + Processes and handles incoming commands. + + **Path parameters:** + - command (CommandType): The type of the command. + + **Request body:** + data (dict): The data associated with the command. + + **Returns:** + The OCPIResponse indicating the success or failure of the command. + + **Raises:** + - HTTPException: If there is a validation error or if + the command action returns without a result. + - NotFoundOCPIError: If the associated location is not found. + """ + logger.info(f"Received command - `{command}`.") + logger.debug(f"Command data - {data}") auth_token = get_auth_token(request) try: command_data = await apply_pydantic_schema(command, data) except ValidationError as exc: + logger.debug("ValidationError on applying pydantic schema to command") return JSONResponse( status_code=fastapistatus.HTTP_422_UNPROCESSABLE_ENTITY, - content={'detail': jsonable_encoder(exc.errors())} + content={"detail": jsonable_encoder(exc.errors())}, ) try: - if hasattr(command_data, 'location_id'): - await crud.get(ModuleID.locations, RoleEnum.cpo, command_data.location_id, auth_token=auth_token, - version=VersionNumber.v_2_2_1) - - command_response = await crud.do(ModuleID.commands, RoleEnum.cpo, Action.send_command, command_data.dict(), - command=command, auth_token=auth_token, version=VersionNumber.v_2_2_1) - - background_tasks.add_task(send_command_result, command_data=command_data, command=command, - auth_token=auth_token, crud=crud, adapter=adapter) - + if hasattr(command_data, "location_id"): + location = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + command_data.location_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + if not location: + raise NotFoundOCPIError + + command_response = await crud.do( + ModuleID.commands, + RoleEnum.cpo, + Action.send_command, + command_data.dict(), + command=command, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + if command_response: + if command_response["result"] == CommandResponseType.accepted: + background_tasks.add_task( + send_command_result, + command_data=command_data, + command=command, + auth_token=auth_token, + crud=crud, + adapter=adapter, + ) + return OCPIResponse( + data=[ + adapter.command_response_adapter(command_response).dict() + ], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug("Send command action returned without result.") + command_response = CommandResponse( + result=CommandResponseType.rejected, timeout=0 + ) return OCPIResponse( - data=[adapter.command_response_adapter(command_response).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + data=[command_response.dict()], + **status.OCPI_3000_GENERIC_SERVER_ERROR, ) # when the location is not found except NotFoundOCPIError: - command_response = CommandResponse(result=CommandResponseType.rejected, timeout=0) + logger.info( + f"Location with id `{command_data.location_id}` was not found." + ) + command_response = CommandResponse( + result=CommandResponseType.rejected, timeout=0 + ) return OCPIResponse( data=[command_response.dict()], **status.OCPI_2003_UNKNOWN_LOCATION, diff --git a/py_ocpi/modules/commands/v_2_2_1/api/emsp.py b/py_ocpi/modules/commands/v_2_2_1/api/emsp.py index b6825e7..aa386eb 100644 --- a/py_ocpi/modules/commands/v_2_2_1/api/emsp.py +++ b/py_ocpi/modules/commands/v_2_2_1/api/emsp.py @@ -1,27 +1,57 @@ from fastapi import APIRouter, Depends, Request -from py_ocpi.core.dependencies import get_crud, get_adapter +from py_ocpi.core.dependencies import get_crud from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.authentication.verifier import AuthorizationVerifier 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.config import logger from py_ocpi.core import status from py_ocpi.core.utils import get_auth_token from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.modules.commands.v_2_2_1.schemas import CommandResult router = APIRouter( - prefix='/commands', + prefix="/commands", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) @router.post("/{uid}", response_model=OCPIResponse) -async def receive_command_result(request: Request, uid: str, command_result: CommandResult, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def receive_command_result( + request: Request, + uid: str, + command_result: CommandResult, + crud: Crud = Depends(get_crud), +): + """ + Receive Command Result. + + Processes and handles incoming command results. + + **Path parameters:** + - uid (str): The unique identifier associated with the command. + + **Request body:** + command_response (CommandResponse): The response data + associated with the command. + + **Returns:** + The OCPIResponse indicating the success or failure of + processing the command result. + """ + logger.info(f"Received command result with uid - `{uid}`.") + logger.debug(f"Command result data - {command_result.dict()}") auth_token = get_auth_token(request) - await crud.update(ModuleID.commands, RoleEnum.emsp, command_result.dict(), uid, - auth_token=auth_token, version=VersionNumber.v_2_2_1) + await crud.update( + ModuleID.commands, + RoleEnum.emsp, + command_result.dict(), + uid, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[], diff --git a/py_ocpi/modules/commands/v_2_2_1/enums.py b/py_ocpi/modules/commands/v_2_2_1/enums.py index 2dc118e..106907f 100644 --- a/py_ocpi/modules/commands/v_2_2_1/enums.py +++ b/py_ocpi/modules/commands/v_2_2_1/enums.py @@ -5,14 +5,16 @@ class CommandResponseType(str, Enum): """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_commands.asciidoc#141-commandresponsetype-enum """ - # The requested command is not supported by this CPO, Charge Point, EVSE etc. - not_supported = 'NOT_SUPPORTED' - # Command request rejected by the CPO. (Session might not be from a customer of the eMSP that send this request) - rejected = 'REJECTED' + # The requested command is not supported by this CPO, + # Charge Point, EVSE etc. + not_supported = "NOT_SUPPORTED" + # Command request rejected by the CPO. + # (Session might not be from a customer of the eMSP that send this request) + rejected = "REJECTED" # Command request accepted by the CPO. - accepted = 'ACCEPTED' + accepted = "ACCEPTED" # The Session in the requested command is not known by this CPO - unknown_session = 'UNKNOWN_SESSION' + unknown_session = "UNKNOWN_SESSION" class CommandResultType(str, Enum): @@ -20,23 +22,26 @@ class CommandResultType(str, Enum): https://github.com/ocpi/ocpi/blob/2.2.1/mod_commands.asciidoc#142-commandresulttype-enum """ # Command request accepted by the Charge Point. - accepted = 'ACCEPTED' + accepted = "ACCEPTED" # The Reservation has been canceled by the CPO. - canceled_reservation = 'CANCELED_RESERVATION' - # EVSE is currently occupied, another session is ongoing. Cannot start a new session - evse_occupied = 'EVSE_OCCUPIED' + canceled_reservation = "CANCELED_RESERVATION" + # EVSE is currently occupied, another session is ongoing. + # Cannot start a new session + evse_occupied = "EVSE_OCCUPIED" # EVSE is currently inoperative or faulted. - evse_inoperative = 'EVSE_INOPERATIVE' + evse_inoperative = "EVSE_INOPERATIVE" # Execution of the command failed at the Charge Point. - failed = 'FAILED' + failed = "FAILED" # The requested command is not supported by this Charge Point, EVSE etc. - not_supported = 'NOT_SUPPORTED' + not_supported = "NOT_SUPPORTED" # Command request rejected by the Charge Point. - rejected = 'REJECTED' - # Command request timeout, no response received from the Charge Point in a reasonable time. - timeout = 'TIMEOUT' - # The Reservation in the requested command is not known by this Charge Point. - unknown_reservation = 'UNKNOWN_RESERVATION' + rejected = "REJECTED" + # Command request timeout, no response received + # from the Charge Point in a reasonable time. + timeout = "TIMEOUT" + # The Reservation in the requested command + # is not known by this Charge Point. + unknown_reservation = "UNKNOWN_RESERVATION" class CommandType(str, Enum): @@ -44,13 +49,15 @@ class CommandType(str, Enum): https://github.com/ocpi/ocpi/blob/2.2.1/mod_commands.asciidoc#143-commandtype-enum """ # Request the Charge Point to cancel a specific reservation. - cancel_reservation = 'CANCEL_RESERVATION' - # Request the Charge Point to reserve a (specific) EVSE for a Token for a certain time, starting now. - reserve_now = 'RESERVE_NOW' - # Request the Charge Point to start a transaction on the given EVSE/Connector. - start_session = 'START_SESSION' + cancel_reservation = "CANCEL_RESERVATION" + # Request the Charge Point to reserve a (specific) + # EVSE for a Token for a certain time, starting now. + reserve_now = "RESERVE_NOW" + # Request the Charge Point to start a transaction on the given + # EVSE/Connector. + start_session = "START_SESSION" # Request the Charge Point to stop an ongoing session. - stop_session = 'STOP_SESSION' + stop_session = "STOP_SESSION" # Request the Charge Point to unlock the connector (if applicable). # This functionality is for help desk operators only! - unlock_connector = 'UNLOCK_CONNECTOR' + unlock_connector = "UNLOCK_CONNECTOR" diff --git a/py_ocpi/modules/commands/v_2_2_1/schemas.py b/py_ocpi/modules/commands/v_2_2_1/schemas.py index 90f0e4e..13d8561 100644 --- a/py_ocpi/modules/commands/v_2_2_1/schemas.py +++ b/py_ocpi/modules/commands/v_2_2_1/schemas.py @@ -2,7 +2,10 @@ from pydantic import BaseModel from py_ocpi.core.data_types import CiString, URL, DisplayText, DateTime -from py_ocpi.modules.commands.v_2_2_1.enums import CommandResponseType, CommandResultType +from py_ocpi.modules.commands.v_2_2_1.enums import ( + CommandResponseType, + CommandResultType, +) from py_ocpi.modules.tokens.v_2_2_1.schemas import Token diff --git a/py_ocpi/modules/credentials/v_2_1_1/api/__init__.py b/py_ocpi/modules/credentials/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/credentials/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/credentials/v_2_1_1/api/cpo.py b/py_ocpi/modules/credentials/v_2_1_1/api/cpo.py new file mode 100644 index 0000000..6b2c130 --- /dev/null +++ b/py_ocpi/modules/credentials/v_2_1_1/api/cpo.py @@ -0,0 +1,352 @@ +from typing import Union + +import httpx +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Request, + status as fastapistatus, +) + +from py_ocpi.core import status +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import ( + AuthorizationVerifier, + CredentialsAuthorizationVerifier, +) +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +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 py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.credentials.v_2_1_1.schemas import Credentials + +router = APIRouter( + prefix="/credentials", +) +cred_dependency = CredentialsAuthorizationVerifier(VersionNumber.v_2_1_1) + + +@router.get( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) +async def get_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get credentials. + + Retrieves credentials based on the specified parameters. + + **Returns:** + The OCPIResponse containing the credentials. + """ + logger.info("Received request to get credentials") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + return OCPIResponse( + data=adapter.credentials_adapter(data, VersionNumber.v_2_1_1).dict(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.post("/", response_model=OCPIResponse) +async def post_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Create credentials. + + Creates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the new credentials. + + **Raises:** + HTTPException: If the client is already registered + (HTTP 405 Method Not Allowed) or if the token is not valid + (HTTP 401 Unauthorized). + """ + logger.info("Received request to create credentials.") + logger.debug(f"POST credentials body: {credentials.dict()}") + + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + # Check if the client is already registered + if server_cred: + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is already registered", + ) + if server_cred is None: + logger.info("Token is not valid.") + + raise HTTPException( + fastapistatus.HTTP_401_UNAUTHORIZED, + "Unauthorized", + ) + + # Retrieve the versions and endpoints from the client + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = f"Token {credentials_client_token}" + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) + + if response_versions.status_code == fastapistatus.HTTP_200_OK: + version_url = None + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") + + for version in versions: + if version["version"] == VersionNumber.v_2_1_1: + version_url = version["url"] + + if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_1_1} is not supported" + ) + + return OCPIResponse( + data=[], + **status.OCPI_3002_UNSUPPORTED_VERSION, + ) + + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) + + if response_endpoints.status_code == fastapistatus.HTTP_200_OK: + # Store client credentials and generate new credentials for sender + endpoints = response_endpoints.json()["data"] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + + new_credentials = await crud.create( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + {"credentials": credentials.dict(), "endpoints": endpoints}, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=adapter.credentials_adapter( + new_credentials, VersionNumber.v_2_1_1 + ).dict(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + return OCPIResponse( + data=[], + **status.OCPI_3001_UNABLE_TO_USE_CLIENTS_API, + ) + + +@router.put("/", response_model=OCPIResponse) +async def update_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Update credentials. + + Updates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the updated credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to update credentials.") + logger.debug(f"PUT credentials body: {credentials}") + + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + # Check if the client is already registered + if not server_cred: + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) + + # Retrieve the versions and endpoints from the client + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = f"Token {credentials_client_token}" + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) + + if response_versions.status_code == fastapistatus.HTTP_200_OK: + version_url = None + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") + + for version in versions: + if version["version"] == VersionNumber.v_2_1_1: + version_url = version["url"] + + if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_1_1} is not supported" + ) + + return OCPIResponse( + data=[], + **status.OCPI_3002_UNSUPPORTED_VERSION, + ) + + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) + + if response_endpoints.status_code == fastapistatus.HTTP_200_OK: + # Update server credentials to access client's + # system and generate new credentials token + endpoints = response_endpoints.json()["data"] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + + new_credentials = await crud.update( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + {"credentials": credentials.dict(), "endpoints": endpoints}, + None, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=adapter.credentials_adapter( + new_credentials, VersionNumber.v_2_1_1 + ).dict(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + return OCPIResponse( + data=[], + **status.OCPI_3001_UNABLE_TO_USE_CLIENTS_API, + ) + + +@router.delete( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) +async def remove_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Remove credentials. + + Deletes credentials based on the specified parameters. + + **Returns:** + The OCPIResponse indicating the successful removal of credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to delete credentials") + + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if not data: + logger.info("Client is not registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) + + await crud.delete( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/credentials/v_2_1_1/api/emsp.py b/py_ocpi/modules/credentials/v_2_1_1/api/emsp.py new file mode 100644 index 0000000..e50545c --- /dev/null +++ b/py_ocpi/modules/credentials/v_2_1_1/api/emsp.py @@ -0,0 +1,351 @@ +from typing import Union + +import httpx +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Request, + status as fastapistatus, +) + +from py_ocpi.core import status +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import ( + AuthorizationVerifier, + CredentialsAuthorizationVerifier, +) +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +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 py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.credentials.v_2_1_1.schemas import Credentials + +router = APIRouter( + prefix="/credentials", +) +cred_dependency = CredentialsAuthorizationVerifier(VersionNumber.v_2_1_1) + + +@router.get( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) +async def get_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get credentials. + + Retrieves credentials based on the specified parameters. + + **Returns:** + The OCPIResponse containing the credentials. + """ + logger.info("Received request to get credentials") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + return OCPIResponse( + data=adapter.credentials_adapter(data, VersionNumber.v_2_1_1).dict(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.post("/", response_model=OCPIResponse) +async def post_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Create credentials. + + Creates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the new credentials. + + **Raises:** + HTTPException: If the client is already registered + (HTTP 405 Method Not Allowed) + or if the token is not valid (HTTP 401 Unauthorized). + """ + logger.info("Received request to create credentials.") + logger.debug(f"POST credentials body: {credentials}") + + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + # Check if the client is already registered + if server_cred: + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is already registered", + ) + if server_cred is None: + logger.info("Token is not valid.") + + raise HTTPException( + fastapistatus.HTTP_401_UNAUTHORIZED, + "Unauthorized", + ) + + # Retrieve the versions and endpoints from the client + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = f"Token {credentials_client_token}" + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) + + if response_versions.status_code == fastapistatus.HTTP_200_OK: + version_url = None + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") + + for version in versions: + if version["version"] == VersionNumber.v_2_1_1: + version_url = version["url"] + + if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_1_1} is not supported" + ) + + return OCPIResponse( + data=[], + **status.OCPI_3002_UNSUPPORTED_VERSION, + ) + + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) + + if response_endpoints.status_code == fastapistatus.HTTP_200_OK: + # Store client credentials and generate new credentials for sender + endpoints = response_endpoints.json()["data"] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + + new_credentials = await crud.create( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + {"credentials": credentials.dict(), "endpoints": endpoints}, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=adapter.credentials_adapter( + new_credentials, VersionNumber.v_2_1_1 + ).dict(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + return OCPIResponse( + data=[], + **status.OCPI_3001_UNABLE_TO_USE_CLIENTS_API, + ) + + +@router.put("/", response_model=OCPIResponse) +async def update_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Update credentials. + + Updates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the updated credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to update credentials.") + logger.debug(f"PUT credentials body: {credentials}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + # Check if the client is already registered + if not server_cred: + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) + + # Retrieve the versions and endpoints from the client + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = f"Token {credentials_client_token}" + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) + + if response_versions.status_code == fastapistatus.HTTP_200_OK: + version_url = None + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") + + for version in versions: + if version["version"] == VersionNumber.v_2_1_1: + version_url = version["url"] + + if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_1_1} is not supported" + ) + + return OCPIResponse( + data=[], + **status.OCPI_3002_UNSUPPORTED_VERSION, + ) + + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) + + if response_endpoints.status_code == fastapistatus.HTTP_200_OK: + # Update server credentials to access client's + # system and generate new credentials token + endpoints = response_endpoints.json()["data"] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + + new_credentials = await crud.update( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + {"credentials": credentials.dict(), "endpoints": endpoints}, + None, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=adapter.credentials_adapter( + new_credentials, VersionNumber.v_2_1_1 + ).dict(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + return OCPIResponse( + data=[], + **status.OCPI_3001_UNABLE_TO_USE_CLIENTS_API, + ) + + +@router.delete( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) +async def remove_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Remove credentials. + + Deletes credentials based on the specified parameters. + + **Returns:** + The OCPIResponse indicating the successful removal of credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to delete credentials") + + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if not data: + logger.info("Client is not registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) + + await crud.delete( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/credentials/v_2_1_1/enums.py b/py_ocpi/modules/credentials/v_2_1_1/enums.py new file mode 100644 index 0000000..e69de29 diff --git a/py_ocpi/modules/credentials/v_2_1_1/schemas.py b/py_ocpi/modules/credentials/v_2_1_1/schemas.py new file mode 100644 index 0000000..9e632ed --- /dev/null +++ b/py_ocpi/modules/credentials/v_2_1_1/schemas.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel + +from py_ocpi.core.data_types import URL, String +from py_ocpi.modules.locations.v_2_1_1.schemas import BusinessDetails + + +class Credentials(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/credentials.md#21-credentials-object + """ + + token: String(64) # type: ignore + url: URL + business_details: BusinessDetails + party_id: String(3) # type: ignore + country_code: String(2) # type: ignore 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 06d869d..787435f 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 @@ -1,28 +1,63 @@ -import httpx +from typing import Union -from fastapi import APIRouter, Depends, HTTPException, Request, status as fastapistatus +import httpx +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Request, + status as fastapistatus, +) from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import ( + AuthorizationVerifier, + CredentialsAuthorizationVerifier, +) from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger 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 +from py_ocpi.core.enums import ModuleID, RoleEnum from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.modules.credentials.v_2_2_1.schemas import Credentials router = APIRouter( - prefix='/credentials', + prefix="/credentials", ) +cred_dependency = CredentialsAuthorizationVerifier(VersionNumber.v_2_2_1) -@router.get("/", response_model=OCPIResponse) -async def get_credentials(request: Request, crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.get( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) +async def get_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get credentials. + + Retrieves credentials based on the specified parameters. + + **Returns:** + The OCPIResponse containing the credentials. + """ + logger.info("Received request to get credentials") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.credentials_and_registration, RoleEnum.cpo, - auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) + 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(), **status.OCPI_1000_GENERIC_SUCESS_CODE, @@ -30,56 +65,116 @@ async def get_credentials(request: Request, crud: Crud = Depends(get_crud), adap @router.post("/", response_model=OCPIResponse) -async def post_credentials(request: Request, credentials: Credentials, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def post_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Create credentials. + + Creates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the new credentials. + + **Raises:** + HTTPException: If the client is already registered + (HTTP 405 Method Not Allowed) + or if the token is not valid (HTTP 401 Unauthorized). + """ + logger.info("Received request to create credentials.") + logger.debug(f"POST credentials body: {credentials.dict()}") + auth_token = get_auth_token(request) # Check if the client is already registered - credentials_client_token = credentials.token - server_cred = await crud.do(ModuleID.credentials_and_registration, RoleEnum.cpo, Action.get_client_token, - version=VersionNumber.v_2_2_1, auth_token=auth_token) if server_cred: - raise HTTPException(fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, "Client is already registered") + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is already registered", + ) + if server_cred is None: + logger.info("Token is not valid.") + + raise HTTPException( + fastapistatus.HTTP_401_UNAUTHORIZED, + "Unauthorized", + ) # Retrieve the versions and endpoints from the client - async with httpx.AsyncClient() as client: - authorization_token = f'Token {encode_string_base64(credentials_client_token)}' - response_versions = await client.get(credentials.url, - headers={'authorization': authorization_token}) + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = ( + f"Token {encode_string_base64(credentials_client_token)}" + ) + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) if response_versions.status_code == fastapistatus.HTTP_200_OK: version_url = None - versions = response_versions.json()['data'] + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") for version in versions: - if version['version'] == VersionNumber.v_2_2_1: - version_url = version['url'] + if version["version"] == VersionNumber.v_2_2_1: + version_url = version["url"] if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_2_1} is not supported" + ) + return OCPIResponse( data=[], **status.OCPI_3002_UNSUPPORTED_VERSION, ) - response_endpoints = await client.get(version_url, - headers={'authorization': authorization_token}) + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) if response_endpoints.status_code == fastapistatus.HTTP_200_OK: # Store client credentials and generate new credentials for sender - endpoints = response_endpoints.json()['data'] + endpoints = response_endpoints.json()["data"] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + new_credentials = await crud.create( - ModuleID.credentials_and_registration, RoleEnum.cpo, - { - "credentials": credentials.dict(), - "endpoints": endpoints - }, + ModuleID.credentials_and_registration, + RoleEnum.cpo, + {"credentials": credentials.dict(), "endpoints": endpoints}, auth_token=auth_token, - version=VersionNumber.v_2_2_1 + version=VersionNumber.v_2_2_1, ) return OCPIResponse( data=adapter.credentials_adapter(new_credentials).dict(), - **status.OCPI_1000_GENERIC_SUCESS_CODE + **status.OCPI_1000_GENERIC_SUCESS_CODE, ) return OCPIResponse( @@ -89,53 +184,110 @@ async def post_credentials(request: Request, credentials: Credentials, @router.put("/", response_model=OCPIResponse) -async def update_credentials(request: Request, credentials: Credentials, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def update_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Update credentials. + + Updates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the updated credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to update credentials.") + logger.debug(f"PUT credentials body: {credentials.dict()}") auth_token = get_auth_token(request) # Check if the client is already registered - credentials_client_token = credentials.token - server_cred = await crud.do(ModuleID.credentials_and_registration, RoleEnum.cpo, Action.get_client_token, - version=VersionNumber.v_2_2_1, auth_token=auth_token) if not server_cred: - raise HTTPException(fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, "Client is not registered") + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) # Retrieve the versions and endpoints from the client - async with httpx.AsyncClient() as client: - authorization_token = f'Token {encode_string_base64(credentials_client_token)}' - response_versions = await client.get(credentials.url, headers={'authorization': authorization_token}) + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = ( + f"Token {encode_string_base64(credentials_client_token)}" + ) + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) if response_versions.status_code == fastapistatus.HTTP_200_OK: version_url = None - versions = response_versions.json()['data'] + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") for version in versions: - if version['version'] == VersionNumber.v_2_2_1: - version_url = version['url'] + if version["version"] == VersionNumber.v_2_2_1: + version_url = version["url"] if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_2_1} is not supported" + ) + return OCPIResponse( data=[], **status.OCPI_3002_UNSUPPORTED_VERSION, ) - response_endpoints = await client.get(version_url, - headers={'authorization': authorization_token}) + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) if response_endpoints.status_code == fastapistatus.HTTP_200_OK: - # Update server credentials to access client's system and generate new credentials token - endpoints = response_endpoints.json()['data'][0] - new_credentials = await crud.update(ModuleID.credentials_and_registration, RoleEnum.cpo, - { - "credentials": credentials.dict(), - "endpoints": endpoints - }, - auth_token=auth_token, - version=VersionNumber.v_2_2_1) + # Update server credentials to access client's + # system and generate new credentials token + endpoints = response_endpoints.json()["data"][0] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + + new_credentials = await crud.update( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + {"credentials": credentials.dict(), "endpoints": endpoints}, + # TODO check credential_id + id="", + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=adapter.credentials_adapter(new_credentials).dict(), - **status.OCPI_1000_GENERIC_SUCESS_CODE + **status.OCPI_1000_GENERIC_SUCESS_CODE, ) return OCPIResponse( @@ -144,17 +296,54 @@ async def update_credentials(request: Request, credentials: Credentials, ) -@router.delete("/", response_model=OCPIResponse) -async def remove_credentials(request: Request, crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.delete( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) +async def remove_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Remove credentials. + + Deletes credentials based on the specified parameters. + + **Returns:** + The OCPIResponse indicating the successful removal of credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to delete credentials") + auth_token = get_auth_token(request) - data = await crud.get(ModuleID.credentials_and_registration, RoleEnum.cpo, - auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) + data = await crud.get( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) if not data: - raise HTTPException(fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, "Client is not registered") - - await crud.delete(ModuleID.credentials_and_registration, RoleEnum.cpo, - auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) + logger.info("Client is not registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) + + await crud.delete( + ModuleID.credentials_and_registration, + RoleEnum.cpo, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[], 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 e713e6e..5d65965 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 @@ -1,28 +1,63 @@ -import httpx +from typing import Union -from fastapi import APIRouter, Depends, HTTPException, Request, status as fastapistatus +import httpx +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Request, + status as fastapistatus, +) from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import ( + AuthorizationVerifier, + CredentialsAuthorizationVerifier, +) from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger 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 +from py_ocpi.core.enums import ModuleID, RoleEnum from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.modules.credentials.v_2_2_1.schemas import Credentials router = APIRouter( - prefix='/credentials', + prefix="/credentials", ) +cred_dependency = CredentialsAuthorizationVerifier(VersionNumber.v_2_2_1) -@router.get("/", response_model=OCPIResponse) -async def get_credentials(request: Request, crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.get( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) +async def get_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get credentials. + + Retrieves credentials based on the specified parameters. + + **Returns:** + The OCPIResponse containing the credentials. + """ + logger.info("Received request to get credentials") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.credentials_and_registration, RoleEnum.emsp, - auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) + 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(), **status.OCPI_1000_GENERIC_SUCESS_CODE, @@ -30,56 +65,116 @@ async def get_credentials(request: Request, crud: Crud = Depends(get_crud), adap @router.post("/", response_model=OCPIResponse) -async def post_credentials(request: Request, credentials: Credentials, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def post_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Create credentials. + + Creates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the new credentials. + + **Raises:** + HTTPException: If the client is already registered + (HTTP 405 Method Not Allowed) + or if the token is not valid (HTTP 401 Unauthorized). + """ + logger.info("Received request to create credentials.") + logger.debug(f"POST credentials body: {credentials.dict()}") + auth_token = get_auth_token(request) # Check if the client is already registered - credentials_client_token = credentials.token - server_cred = await crud.do(ModuleID.credentials_and_registration, RoleEnum.emsp, Action.get_client_token, - version=VersionNumber.v_2_2_1, auth_token=auth_token) if server_cred: - raise HTTPException(fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, "Client is already registered") + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is already registered", + ) + if server_cred is None: + logger.info("Token is not valid.") + + raise HTTPException( + fastapistatus.HTTP_401_UNAUTHORIZED, + "Unauthorized", + ) # Retrieve the versions and endpoints from the client - async with httpx.AsyncClient() as client: - authorization_token = f'Token {encode_string_base64(credentials_client_token)}' - response_versions = await client.get(credentials.url, - headers={'authorization': authorization_token}) + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = ( + f"Token {encode_string_base64(credentials_client_token)}" + ) + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) if response_versions.status_code == fastapistatus.HTTP_200_OK: version_url = None - versions = response_versions.json()['data'] + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") for version in versions: - if version['version'] == VersionNumber.v_2_2_1: - version_url = version['url'] + if version["version"] == VersionNumber.v_2_2_1: + version_url = version["url"] if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_2_1} is not supported" + ) + return OCPIResponse( data=[], **status.OCPI_3002_UNSUPPORTED_VERSION, ) - response_endpoints = await client.get(version_url, - headers={'authorization': authorization_token}) + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) if response_endpoints.status_code == fastapistatus.HTTP_200_OK: # Store client credentials and generate new credentials for sender - endpoints = response_endpoints.json()['data'] + endpoints = response_endpoints.json()["data"] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + new_credentials = await crud.create( - ModuleID.credentials_and_registration, RoleEnum.emsp, - { - "credentials": credentials.dict(), - "endpoints": endpoints - }, + ModuleID.credentials_and_registration, + RoleEnum.emsp, + {"credentials": credentials.dict(), "endpoints": endpoints}, auth_token=auth_token, - version=VersionNumber.v_2_2_1 + version=VersionNumber.v_2_2_1, ) return OCPIResponse( data=adapter.credentials_adapter(new_credentials).dict(), - **status.OCPI_1000_GENERIC_SUCESS_CODE + **status.OCPI_1000_GENERIC_SUCESS_CODE, ) return OCPIResponse( @@ -89,53 +184,110 @@ async def post_credentials(request: Request, credentials: Credentials, @router.put("/", response_model=OCPIResponse) -async def update_credentials(request: Request, credentials: Credentials, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def update_credentials( + request: Request, + credentials: Credentials, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Update credentials. + + Updates credentials based on the specified parameters. + + **Request body:** + credentials (Credentials): The credentials object. + + **Returns:** + The OCPIResponse containing the updated credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to update credentials.") + logger.debug(f"PUT credentials body: {credentials.dict()}") auth_token = get_auth_token(request) # Check if the client is already registered - credentials_client_token = credentials.token - server_cred = await crud.do(ModuleID.credentials_and_registration, RoleEnum.emsp, Action.get_client_token, - version=VersionNumber.v_2_2_1, auth_token=auth_token) if not server_cred: - raise HTTPException(fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, "Client is not registered") + logger.info("Client already registered.") + + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) # Retrieve the versions and endpoints from the client - async with httpx.AsyncClient() as client: - authorization_token = f'Token {encode_string_base64(credentials_client_token)}' - response_versions = await client.get(credentials.url, headers={'authorization': authorization_token}) + async with httpx.AsyncClient() as client: # nosec + credentials_client_token = credentials.token + authorization_token = ( + f"Token {encode_string_base64(credentials_client_token)}" + ) + + logger.info(f"Send request to get versions: {credentials.url}") + + response_versions = await client.get( + credentials.url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET versions status_code: {response_versions.status_code}" + ) if response_versions.status_code == fastapistatus.HTTP_200_OK: version_url = None - versions = response_versions.json()['data'] + versions = response_versions.json()["data"] + + logger.debug(f"GET versions response data: {versions}") for version in versions: - if version['version'] == VersionNumber.v_2_2_1: - version_url = version['url'] + if version["version"] == VersionNumber.v_2_2_1: + version_url = version["url"] if not version_url: + logger.debug( + f"Version {VersionNumber.v_2_2_1} is not supported" + ) + return OCPIResponse( data=[], **status.OCPI_3002_UNSUPPORTED_VERSION, ) - response_endpoints = await client.get(version_url, - headers={'authorization': authorization_token}) + logger.info(f"Send request to get version details: {version_url}") + + response_endpoints = await client.get( + version_url, headers={"authorization": authorization_token} + ) + + logger.info( + f"GET version details status_code: {response_endpoints.status_code}" + ) if response_endpoints.status_code == fastapistatus.HTTP_200_OK: - # Update server credentials to access client's system and generate new credentials token - endpoints = response_endpoints.json()['data'][0] - new_credentials = await crud.update(ModuleID.credentials_and_registration, RoleEnum.emsp, - { - "credentials": credentials.dict(), - "endpoints": endpoints - }, - auth_token=auth_token, - version=VersionNumber.v_2_2_1) + # Update server credentials to access client's + # system and generate new credentials token + endpoints = response_endpoints.json()["data"][0] + + logger.debug( + f"GET version details response data: {endpoints}" + ) + + new_credentials = await crud.update( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + {"credentials": credentials.dict(), "endpoints": endpoints}, + # TODO check credential_id + id="", + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=adapter.credentials_adapter(new_credentials).dict(), - **status.OCPI_1000_GENERIC_SUCESS_CODE + **status.OCPI_1000_GENERIC_SUCESS_CODE, ) return OCPIResponse( @@ -144,18 +296,52 @@ async def update_credentials(request: Request, credentials: Credentials, ) -@router.delete("/", response_model=OCPIResponse) -async def remove_credentials(request: Request, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.delete( + "/", + response_model=OCPIResponse, + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) +async def remove_credentials( + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Remove credentials. + + Deletes credentials based on the specified parameters. + + **Returns:** + The OCPIResponse indicating the successful removal of credentials. + + **Raises:** + HTTPException: If the client is not registered + (HTTP 405 Method Not Allowed). + """ + logger.info("Received request to delete credentials") + auth_token = get_auth_token(request) - data = await crud.get(ModuleID.credentials_and_registration, RoleEnum.emsp, - auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) + data = await crud.get( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) if not data: - raise HTTPException(fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, "Client is not registered") - - await crud.delete(ModuleID.credentials_and_registration, RoleEnum.emsp, - auth_token, auth_token=auth_token, version=VersionNumber.v_2_2_1) + raise HTTPException( + fastapistatus.HTTP_405_METHOD_NOT_ALLOWED, + "Client is not registered", + ) + + await crud.delete( + ModuleID.credentials_and_registration, + RoleEnum.emsp, + auth_token, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[], diff --git a/py_ocpi/modules/hubclientinfo/__init__.py b/py_ocpi/modules/hubclientinfo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py_ocpi/modules/hubclientinfo/v_2_2_1/api/__init__.py b/py_ocpi/modules/hubclientinfo/v_2_2_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/hubclientinfo/v_2_2_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/hubclientinfo/v_2_2_1/api/cpo.py b/py_ocpi/modules/hubclientinfo/v_2_2_1/api/cpo.py new file mode 100644 index 0000000..43b3e4b --- /dev/null +++ b/py_ocpi/modules/hubclientinfo/v_2_2_1/api/cpo.py @@ -0,0 +1,141 @@ +from fastapi import APIRouter, Depends, Request + +from py_ocpi.core.utils import get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.data_types import CiString +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.dependencies import get_crud, get_adapter +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.hubclientinfo.v_2_2_1.schemas import ClientInfo + +router = APIRouter( + prefix="/clientinfo", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) + + +@router.get("/{country_code}/{party_id}", response_model=OCPIResponse) +async def get_hubclientinfo( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Hub Client Info. + + Gets information about the hub client with the specified + country code and party ID. + + **Parameters:** + - country_code (str): The country code of the hub client. + - party_id (str): The party ID of the hub client. + + **Returns:** + The OCPIResponse containing information about the hub client. + + **Raises:** + - NotFoundOCPIError: If the hub client info is not found. + """ + logger.info( + f"Received request to get hub client info with country code - `{country_code}` " + f"and party id - `{party_id}`." + ) + auth_token = get_auth_token(request) + + data = await crud.get( + ModuleID.hub_client_info, + RoleEnum.cpo, + None, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + if data: + return OCPIResponse( + data=[adapter.hubclientinfo_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.info("Hub client info was not found.") + raise NotFoundOCPIError + + +@router.put("/{country_code}/{party_id}", response_model=OCPIResponse) +async def add_or_update_clienthubinfo( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + client_hub_info: ClientInfo, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Hub Client Info. + + Adds or updates information about the hub client with the specified + country code and party ID. + + **Parameters:** + - country_code (str): The country code of the hub client. + - party_id (str): The party ID of the hub client. + + **Request body:** + - client_hub_info (ClientInfo): The data to update or create + for the hub client. + + **Returns:** + The OCPIResponse containing the updated or created information + about the hub client. + """ + logger.info( + "Received request to add or update hub client info " + f"with country code - `{country_code}` and party id - `{party_id}`." + ) + logger.debug(f"Client hub info data to update - {client_hub_info.dict()}") + auth_token = get_auth_token(request) + + data = await crud.get( + ModuleID.hub_client_info, + RoleEnum.cpo, + None, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + if data: + logger.debug("Update client hub info.") + data = await crud.update( + ModuleID.hub_client_info, + RoleEnum.cpo, + client_hub_info.dict(), + None, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + else: + logger.debug("Create client hub info.") + data = await crud.create( + ModuleID.hub_client_info, + RoleEnum.cpo, + client_hub_info.dict(), + None, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[adapter.hubclientinfo_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/hubclientinfo/v_2_2_1/api/emsp.py b/py_ocpi/modules/hubclientinfo/v_2_2_1/api/emsp.py new file mode 100644 index 0000000..8f01f28 --- /dev/null +++ b/py_ocpi/modules/hubclientinfo/v_2_2_1/api/emsp.py @@ -0,0 +1,141 @@ +from fastapi import APIRouter, Depends, Request + +from py_ocpi.core.utils import get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.data_types import CiString +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.dependencies import get_crud, get_adapter +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.hubclientinfo.v_2_2_1.schemas import ClientInfo + +router = APIRouter( + prefix="/clientinfo", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) + + +@router.get("/{country_code}/{party_id}", response_model=OCPIResponse) +async def get_hubclientinfo( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Hub Client Info. + + Gets information about the hub client with the specified + country code and party ID. + + **Parameters:** + - country_code (str): The country code of the hub client. + - party_id (str): The party ID of the hub client. + + **Returns:** + The OCPIResponse containing information about the hub client. + + **Raises:** + - NotFoundOCPIError: If the hub client info is not found. + """ + logger.info( + f"Received request to get hub client info with country code - `{country_code}` " + f"and party id - `{party_id}`." + ) + auth_token = get_auth_token(request) + + data = await crud.get( + ModuleID.hub_client_info, + RoleEnum.emsp, + None, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + if data: + return OCPIResponse( + data=[adapter.hubclientinfo_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.info("Hub client info was not found.") + raise NotFoundOCPIError + + +@router.put("/{country_code}/{party_id}", response_model=OCPIResponse) +async def add_or_update_clienthubinfo( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + client_hub_info: ClientInfo, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Hub Client Info. + + Adds or updates information about the hub client + with the specified country code and party ID. + + **Parameters:** + - country_code (str): The country code of the hub client. + - party_id (str): The party ID of the hub client. + + **Request body:** + - client_hub_info (ClientInfo): The data to update or + create for the hub client. + + **Returns:** + The OCPIResponse containing the updated or created + information about the hub client. + """ + logger.info( + "Received request to add or update hub client info " + f"with country code - `{country_code}` and party id - `{party_id}`." + ) + logger.debug(f"Client hub info data to update - {client_hub_info.dict()}") + auth_token = get_auth_token(request) + + data = await crud.get( + ModuleID.hub_client_info, + RoleEnum.emsp, + None, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + if data: + logger.debug("Update client hub info.") + data = await crud.update( + ModuleID.hub_client_info, + RoleEnum.emsp, + client_hub_info.dict(), + None, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + else: + logger.debug("Create client hub info.") + data = await crud.create( + ModuleID.hub_client_info, + RoleEnum.emsp, + client_hub_info.dict(), + None, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[adapter.hubclientinfo_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/hubclientinfo/v_2_2_1/enums.py b/py_ocpi/modules/hubclientinfo/v_2_2_1/enums.py new file mode 100644 index 0000000..f091cfd --- /dev/null +++ b/py_ocpi/modules/hubclientinfo/v_2_2_1/enums.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class ConnectionStatus(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_hub_client_info.asciidoc#151-connectionstatus-enum + """ + connected = "CONNECTED" + offline = "OFFLINE" + planned = "PLANNED" + suspended = "SUSPENDED" diff --git a/py_ocpi/modules/hubclientinfo/v_2_2_1/schemas.py b/py_ocpi/modules/hubclientinfo/v_2_2_1/schemas.py new file mode 100644 index 0000000..4d479ca --- /dev/null +++ b/py_ocpi/modules/hubclientinfo/v_2_2_1/schemas.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel + +from py_ocpi.core.data_types import CiString, DateTime +from py_ocpi.core.enums import RoleEnum +from py_ocpi.modules.hubclientinfo.v_2_2_1.enums import ConnectionStatus + + +class ClientInfo(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_hub_client_info.asciidoc#141-clientinfo-object + """ + party_id: CiString(3) + country_code: CiString(2) + role: RoleEnum + status: ConnectionStatus + last_updated: DateTime diff --git a/py_ocpi/modules/locations/enums.py b/py_ocpi/modules/locations/enums.py new file mode 100644 index 0000000..87ec16b --- /dev/null +++ b/py_ocpi/modules/locations/enums.py @@ -0,0 +1,121 @@ +from enum import Enum + + +class ParkingRestriction(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#417-parkingrestriction-enum + """ + + # Reserved parking spot for electric vehicles. + ev_only = "EV_ONLY" + # Parking is only allowed while plugged in (charging). + plugged = "PLUGGED" + # Reserved parking spot for disabled people with valid ID. + disables = "DISABLED" + # Parking spot for customers/guests only, + # for example in case of a hotel or shop. + customers = "CUSTOMERS" + # Parking spot only suitable for (electric) motorcycles or scooters. + motorcycle = "MOTORCYCLES" + + +class Status(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#420-status-enum + """ + + # The EVSE/Connector is able to start a new charging session. + available = "AVAILABLE" + # The EVSE/Connector is not accessible + # because of a physical barrier, i.e. a car. + blocked = "BLOCKED" + # The EVSE/Connector is in use. + charging = "CHARGING" + # The EVSE/Connector is not yet active, + # or temporarily not available for use, but not broken or defect. + inoperative = "INOPERATIVE" + # The EVSE/Connector is currently out of order, + # some part/components may be broken/defect. + outoforder = "OUTOFORDER" + # The EVSE/Connector is planned, will be operating soon. + planned = "PLANNED" + # The EVSE/Connector was discontinued/removed. + removed = "REMOVED" + # The EVSE/Connector is reserved for a particular EV driver + # and is unavailable for other drivers. + reserved = "RESERVED" + # No status information available (also used when offline). + unknown = "UNKNOWN" + + +class ConnectorFormat(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#144-connectorformat-enum + """ + + # The connector is a socket; the EV user needs to bring a fitting plug. + socket = "SOCKET" + # The connector is an attached cable; + # the EV users car needs to have a fitting inlet. + cable = "CABLE" + + +class ImageCategory(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#415-imagecategory-enum + """ + + # Photo of the physical device that contains one or more EVSEs. + charger = "CHARGER" + # Location entrance photo. Should show the car entrance + # to the location from street side. + entrance = "ENTRANCE" + # Location overview photo. + location = "LOCATION" + # Logo of an associated roaming network to be displayed + # with the EVSE for example in lists, + # maps and detailed information views. + network = "NETWORK" + # Logo of the charge point operator, for example a municipality, + # to be displayed in the EVSEs detailed information + # view or in lists and maps, if no network logo is present. + operator = "OPERATOR" + # Other + other = "OTHER" + # Logo of the charge point owner, for example a local store, + # to be displayed in the EVSEs detailed information view. + owner = "OWNER" + + +class EnergySourceCategory(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#148-energysourcecategory-enum + """ + + # Nuclear power sources. + nuclear = "NUCLEAR" + # All kinds of fossil power sources. + general_fossil = "GENERAL_FOSSIL" + # Fossil power from coal. + coal = "COAL" + # Fossil power from gas. + gas = "GAS" + # All kinds of regenerative power sources. + general_green = "GENERAL_GREEN" + # Regenerative power from PV. + solar = "SOLAR" + # Regenerative power from wind turbines. + wind = "WIND" + # Regenerative power from water turbines. + water = "WATER" + + +class EnvironmentalImpactCategory(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1410-environmentalimpactcategory-enum + """ + + # Produced nuclear waste in grams per kilowatthour. + nuclear_waste = "NUCLEAR_WASTE" + # Exhausted carbon dioxide in grams per kilowatthour. + carbon_dioxide = "CARBON_DIOXIDE" diff --git a/py_ocpi/modules/locations/schemas.py b/py_ocpi/modules/locations/schemas.py new file mode 100644 index 0000000..1ce49e7 --- /dev/null +++ b/py_ocpi/modules/locations/schemas.py @@ -0,0 +1,101 @@ +from typing import List, Optional +from pydantic import BaseModel + +from py_ocpi.modules.locations.enums import ( + EnergySourceCategory, + Status, + EnvironmentalImpactCategory, +) +from py_ocpi.core.data_types import ( + DateTime, + DisplayText, + Number, + String, +) + + +class GeoLocation(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_geolocation_class + """ + + latitude: String(max_length=10) # type: ignore + longitude: String(max_length=11) # type: ignore + + +class AdditionalGeoLocation(GeoLocation): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_additionalgeolocation_class + """ + + name: Optional[DisplayText] + + +class StatusSchedule(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1423-statusschedule-class + """ + + period_begin: DateTime + period_end: Optional[DateTime] + status: Status + + +class RegularHours(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1421-regularhours-class + """ + + weekday: int + period_begin: String(max_length=5) # type: ignore + period_end: String(max_length=5) # type: ignore + + +class ExceptionalPeriod(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1411-exceptionalperiod-class + """ + + period_begin: DateTime + period_end: DateTime + + +class Hours(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_hours_class + """ + + twentyfourseven: bool + regular_hours: List[RegularHours] + exceptional_openings: List[ExceptionalPeriod] = [] + exceptional_closings: List[ExceptionalPeriod] = [] + + +class EnergySource(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#147-energysource-class + """ + + source: EnergySourceCategory + percentage: Number + + +class EnvironmentalImpact(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#149-environmentalimpact-class + """ + + category: EnvironmentalImpactCategory + amount: Number + + +class EnergyMix(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_energymix_class + """ + + is_green_energy: bool + energy_sources: List[EnergySource] + environ_impact: Optional[EnvironmentalImpact] + supplier_name: String(max_length=64) # type: ignore + energy_product_name: String(max_length=64) # type: ignore diff --git a/py_ocpi/modules/locations/v_2_1_1/api/__init__.py b/py_ocpi/modules/locations/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/locations/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/locations/v_2_1_1/api/cpo.py b/py_ocpi/modules/locations/v_2_1_1/api/cpo.py new file mode 100644 index 0000000..dee8d6f --- /dev/null +++ b/py_ocpi/modules/locations/v_2_1_1/api/cpo.py @@ -0,0 +1,221 @@ +from fastapi import APIRouter, Depends, Response, Request + +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.utils import get_list, get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.data_types import String +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters + +router = APIRouter( + prefix="/locations", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get("/", response_model=OCPIResponse) +async def get_locations( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get locations. + + Retrieves a list of locations based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return Locations that have + last_updated after this Date/Time (default=None). + - date_to (datetime): Only return Locations that have + last_updated before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of locations. + """ + logger.info("Received request to get locations.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data_list = await get_list( + response, + filters, + ModuleID.locations, + RoleEnum.cpo, + VersionNumber.v_2_1_1, + crud, + auth_token=auth_token, + ) + + locations = [] + for data in data_list: + locations.append( + adapter.location_adapter(data, VersionNumber.v_2_1_1).dict() + ) + logger.debug(f"Amount of locations in response: {len(locations)}") + return OCPIResponse( + data=locations, + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.get("/{location_id}", response_model=OCPIResponse) +async def get_location( + request: Request, + location_id: String(39), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get location by ID. + + Retrieves location details based on the specified ID. + + **Path parameters:** + - location_id (str): The ID of the location to retrieve (39 characters). + + **Returns:** + The OCPIResponse containing the location details. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID is not found. + """ + logger.info(f"Received request to get location by id - `{location_id}`.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + location_id, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if data: + return OCPIResponse( + data=[adapter.location_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.get("/{location_id}/{evse_uid}", response_model=OCPIResponse) +async def get_evse( + request: Request, + location_id: String(39), # type: ignore + evse_uid: String(39), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get EVSE by ID. + + Retrieves Electric Vehicle Supply Equipment (EVSE) details + based on the specified Location ID and EVSE UID. + + **Path parameters:** + - location_id (str): The ID of the location containing + the EVSE (39 characters). + - evse_uid (str): The UID of the EVSE to retrieve (39 characters). + + **Returns:** + The OCPIResponse containing the EVSE details. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID + or EVSE with the specified UID is not found. + """ + logger.info( + f"Received request to get evse by id - `{location_id}` (location id - `{evse_uid}`)" + ) + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + location_id, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if data: + location = adapter.location_adapter(data, VersionNumber.v_2_1_1) + for evse in location.evses: + if evse.uid == evse_uid: + return OCPIResponse( + data=[evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.get( + "/{location_id}/{evse_uid}/{connector_id}", response_model=OCPIResponse +) +async def get_connector( + request: Request, + location_id: String(39), # type: ignore + evse_uid: String(39), # type: ignore + connector_id: String(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Connector by ID. + + Retrieves Connector details based on the specified Location ID, + EVSE UID, and Connector ID. + + **Path parameters:** + - location_id (str): The ID of the location containing + the EVSE (39 characters). + - evse_uid (str): The UID of the EVSE to retrieve (39 characters). + - connector_id (str): The ID of the connector + to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the Connector details. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID, EVSE with the + specified UID, or Connector with the specified ID is not found. + """ + logger.info( + f"Received request to get connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + location_id, + auth_token=auth_token, + version=VersionNumber.v_2_1_1, + ) + if data: + location = adapter.location_adapter(data, VersionNumber.v_2_1_1) + for evse in location.evses: + if evse.uid == evse_uid: + for connector in evse.connectors: + if connector.id == connector_id: + return OCPIResponse( + data=[connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug( + f"Connector with id `{connector_id}` was not found." + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/locations/v_2_1_1/api/emsp.py b/py_ocpi/modules/locations/v_2_1_1/api/emsp.py new file mode 100644 index 0000000..73b547a --- /dev/null +++ b/py_ocpi/modules/locations/v_2_1_1/api/emsp.py @@ -0,0 +1,716 @@ +import copy + +from fastapi import APIRouter, Depends, Request + +from py_ocpi.core.utils import ( + get_auth_token, + partially_update_attributes, +) +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.data_types import String +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.dependencies import get_crud, get_adapter +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.locations.v_2_1_1.schemas import ( + Location, + LocationPartialUpdate, + EVSE, + EVSEPartialUpdate, + Connector, + ConnectorPartialUpdate, +) + +router = APIRouter( + prefix="/locations", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get( + "/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse +) +async def get_location( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Location. + + Retrieves a location based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str: The three-letter party ID. + - location_id (str): The ID of the location to retrieve (39 characters). + + **Returns:** + The OCPIResponse containing the location data. + + **Raises:** + NotFoundOCPIError: NotFoundOCPIError: If the location is not found. + """ + logger.info( + f"Received request to get location with id - `{location_id}`." + ) + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + return OCPIResponse( + data=[adapter.location_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.get( + "/{country_code}/{party_id}/{location_id}/{evse_uid}", + response_model=OCPIResponse, +) +async def get_evse( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + evse_uid: String(39), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get EVSE. + + Retrieves an EVSE based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location containing + the EVSE (39 characters). + - evse_uid (str): The UID of the EVSE to retrieve (39 characters). + + **Returns:** + The OCPIResponse containing the EVSE data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID + or EVSE with the specified UID is not found. + """ + logger.info( + f"Received request to get evse by id - `{location_id}` (location id - `{evse_uid}`)" + ) + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + location = adapter.location_adapter(data, VersionNumber.v_2_1_1) + for evse in location.evses: + if evse.uid == evse_uid: + return OCPIResponse( + data=[evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.get( + "/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", + response_model=OCPIResponse, +) +async def get_connector( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + evse_uid: String(39), # type: ignore + connector_id: String(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Connector. + + Retrieves a connector based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location containing + the EVSE (39 characters). + - evse_uid (str): The UID of the EVSE containing + the connector (39 characters). + - connector_id (str): The ID of the connector + to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the connector data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID, EVSE with the + specified UID, or Connector with the specified ID is not found. + """ + logger.info( + f"Received request to get connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + location = adapter.location_adapter(data, VersionNumber.v_2_1_1) + for evse in location.evses: + if evse.uid == evse_uid: + for connector in evse.connectors: + if connector.id == connector_id: + return OCPIResponse( + data=[connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug( + f"Connector with id `{connector_id}` was not found." + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse +) +async def add_or_update_location( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + location: Location, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Location. + + Adds or updates a location based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location to add + or update (39 characters). + + **Request body:** + location (Location): The location object. + + **Returns:** + The OCPIResponse containing the added or updated location data. + + **Raises:** + NotFoundOCPIError: If the location is not found. + """ + logger.info( + f"Received request to add or update location with id - `{location_id}`." + ) + logger.debug(f"Location data to update - {location.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + logger.debug(f"Update location with id - `{location_id}`.") + data = await crud.update( + ModuleID.locations, + RoleEnum.emsp, + location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + else: + logger.debug(f"Create location with id - `{location_id}`.") + data = await crud.create( + ModuleID.locations, + RoleEnum.emsp, + location.dict(), + auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[adapter.location_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.put( + "/{country_code}/{party_id}/{location_id}/{evse_uid}", + response_model=OCPIResponse, +) +async def add_or_update_evse( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + evse_uid: String(39), # type: ignore + evse: EVSE, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update EVSE. + + Adds or updates an EVSE based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location (39 characters). + - evse_uid (str): The ID of the EVSE to add or update (39 characters). + + **Request body:** + evse (EVSE): The EVSE object. + + **Returns:** + The OCPIResponse containing the added or updated EVSE data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID is not found. + """ + logger.info( + f"Received request to add or update evse by id - `{location_id}` " + f"(location id - `{evse_uid}`)" + ) + logger.debug(f"Evse data to update - {evse.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if old_data: + old_location = adapter.location_adapter(old_data, VersionNumber.v_2_1_1) + new_location = copy.deepcopy(old_location) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + logger.debug(f"Update evse with id - {evse_uid}") + new_location.evses.remove(old_evse) + break + + new_location.evses.append(evse) + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", + response_model=OCPIResponse, +) +async def add_or_update_connector( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + evse_uid: String(39), # type: ignore + connector_id: String(36), # type: ignore + connector: Connector, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Connector. + + Adds or updates a connector based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location (39 characters). + - evse_uid (str): The ID of the EVSE (39 characters). + - connector_id (str): The ID of the connector to add + or update (36 characters). + + **Request body:** + connector (Connector): The connector object. + + **Returns:** + The OCPIResponse containing the added or updated connector data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID or EVSE + with the specified UID is not found. + """ + logger.info( + f"Received request to get connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) + logger.debug(f"Connector data to update - {connector.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if old_data: + old_location = adapter.location_adapter(old_data, VersionNumber.v_2_1_1) + new_location = copy.deepcopy(old_location) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + new_location.evses.remove(old_evse) + new_evse = copy.deepcopy(old_evse) + for old_connector in old_evse.connectors: + if old_connector.id == connector_id: + logger.debug( + f"Update connector with id - {connector_id}" + ) + new_evse.connectors.remove(old_connector) + break + new_evse.connectors.append(connector) + new_location.evses.append(new_evse) + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.patch( + "/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse +) +async def partial_update_location( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + location: LocationPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Location. + + Partially updates a location based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location + to partially update (39 characters). + + **Request body:** + location (LocationPartialUpdate): The partial location update object. + + **Returns:** + The OCPIResponse containing the partially updated location data. + + **Raises:** + NotFoundOCPIError: If the location is not found. + """ + logger.info( + f"Received request to partially update location with id - `{location_id}`." + ) + logger.debug(f"Location data to update - {location.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if old_data: + old_location = adapter.location_adapter(old_data, VersionNumber.v_2_1_1) + new_location = copy.deepcopy(old_location) + + partially_update_attributes( + new_location, + location.dict(exclude_defaults=True, exclude_unset=True), + ) + + data = await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[adapter.location_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.patch( + "/{country_code}/{party_id}/{location_id}/{evse_uid}", + response_model=OCPIResponse, +) +async def partial_update_evse( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(39), # type: ignore + evse_uid: String(39), # type: ignore + evse: EVSEPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update EVSE. + + Partially updates an EVSE based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location + to partially update (36 characters). + - evse_uid (str): The UID of the EVSE + to partially update (39 characters). + + **Request body:** + evse (EVSEPartialUpdate): The partial EVSE update object. + + **Returns:** + The OCPIResponse containing the partially updated EVSE data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID + or EVSE with the specified UID is not found. + """ + logger.info( + f"Received request to partially update evse by id - `{location_id}` " + f"(location id - `{evse_uid}`)" + ) + logger.debug(f"Evse data to update - {evse.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if old_data: + old_location = adapter.location_adapter(old_data, VersionNumber.v_2_1_1) + new_location = copy.deepcopy(old_location) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + new_location.evses.remove(old_evse) + new_evse = copy.deepcopy(old_evse) + partially_update_attributes( + new_evse, + evse.dict(exclude_defaults=True, exclude_unset=True), + ) + new_location.evses.append(new_evse) + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + return OCPIResponse( + data=[new_evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.patch( + "/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", + response_model=OCPIResponse, +) +async def partial_update_connector( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + location_id: String(36), # type: ignore + evse_uid: String(39), # type: ignore + connector_id: String(36), # type: ignore + connector: ConnectorPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Connector. + + Partially updates a connector based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location + to partially update (36 characters). + - evse_uid (str): The UID of the EVSE + to partially update (39 characters). + - connector_id (str): The ID of the connector + to partially update (36 characters). + + **Request body:** + connector (ConnectorPartialUpdate): The partial connector update object. + + **Returns:** + The OCPIResponse containing the partially updated connector data. + + **Raises:** + NotFoundOCPIError:If the location with the specified ID, EVSE with + the specified UID, or Connector with the specified ID is not found. + """ + logger.info( + f"Received request to partially update connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) + logger.debug(f"Connector data to update - {connector.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if old_data: + old_location = adapter.location_adapter(old_data, VersionNumber.v_2_1_1) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + for old_connector in old_evse.connectors: + if old_connector.id == connector_id: + new_connector = old_connector + partially_update_attributes( + new_connector, + connector.dict( + exclude_defaults=True, exclude_unset=True + ), + ) + new_location = old_location + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[new_connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug( + f"Connector with id `{connector_id}` was not found." + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/locations/v_2_1_1/enums.py b/py_ocpi/modules/locations/v_2_1_1/enums.py new file mode 100644 index 0000000..c72bf99 --- /dev/null +++ b/py_ocpi/modules/locations/v_2_1_1/enums.py @@ -0,0 +1,151 @@ +from py_ocpi.modules.locations.enums import * # noqa + + +class LocationType(str, Enum): # noqa + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#416-locationtype-enum + """ + + # Parking in public space. + on_street = "ON_STREET" + # Multistorey car park. + parking_garage = "PARKING_GARAGE" + # Multistorey car park, mainly underground. + underground_garage = "UNDERGROUND_GARAGE" + # A cleared area that is intended for parking vehicles, i.e. at super + # markets, bars, etc. + parking_lot = "PARKING_LOT" + # None of the given possibilities. + other = "OTHER" + # Parking location type is not known by the operator (default). + unknown = "UNKNOWN" + + +class Facility(str, Enum): # noqa + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#411-facility-enum + """ + + # A hotel. + hotel = "HOTEL" + # A restaurant. + restaurant = "RESTAURANT" + # A cafe. + cafe = "CAFE" + # A mall or shopping center. + mall = "MALL" + # A supermarket. + supermarket = "SUPERMARKET" + # Sport facilities: gym, field etc. + sport = "SPORT" + # A recreation area. + recreation_area = "RECREATION_AREA" + # Located in, or close to, a park, nature reserve etc. + nature = "NATURE" + # A museum. + museum = "MUSEUM" + # A bus stop. + bus_stop = "BUS_STOP" + # A taxi stand. + taxi_stand = "TAXI_STAND" + # A train station. + train_station = "TRAIN_STATION" + # An airport. + airport = "AIRPORT" + # A carpool parking. + carpool_parking = "CARPOOL_PARKING" + # A Fuel station. + fuel_station = "FUEL_STATION" + # Wifi or other type of internet available. + wifi = "WIFI" + + +class Capability(str, Enum): # noqa + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#42-capability-enum + """ + + # The EVSE supports charging profiles. Sending Charging Profiles + # is not yet supported by OCPI. + charging_profile_capable = "CHARGING_PROFILE_CAPABLE" + # Charging at this EVSE can be payed with credit card. + credit_card_payable = "CREDIT_CARD_PAYABLE" + # The EVSE can remotely be started/stopped. + remote_start_stop_capable = "REMOTE_START_STOP_CAPABLE" + # The EVSE can be reserved. + reservable = "RESERVABLE" + # Charging at this EVSE can be authorized with a RFID token + rfid_reader = "RFID_READER" + # Connectors have mechanical lock that can be requested by the eMSP + # to be unlocked. + unlock_capable = "UNLOCK_CAPABLE" + + +class ConnectorType(str, Enum): # noqa + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#44-connectortype-enum + """ + + # The connector type is CHAdeMO, DC + chademo = "CHADEMO" + # Standard/Domestic household, type "A", NEMA 1-15, 2 pins + domestic_a = "DOMESTIC_A" + # Standard/Domestic household, type "B", NEMA 5-15, 3 pins + domestic_b = "DOMESTIC_B" + # Standard/Domestic household, type "C", CEE 7/17, 2 pins + domestic_c = "DOMESTIC_C" + # Standard/Domestic household, type "D", 3 pin + domestic_d = "DOMESTIC_D" + # Standard/Domestic household, type "E", CEE 7/5 3 pins + domestic_e = "DOMESTIC_E" + # Standard/Domestic household, type "F", CEE 7/4, Schuko, 3 pins + domestic_f = "DOMESTIC_F" + # Standard/Domestic household, type "G", BS 1363, Commonwealth, 3 pins + domestic_g = "DOMESTIC_G" + # Standard/Domestic household, type "H", SI-32, 3 pins + domestic_h = "DOMESTIC_H" + # Standard/Domestic household, type "I", AS 3112, 3 pins + domestic_i = "DOMESTIC_I" + # Standard/Domestic household, type "J", SEV 1011, 3 pins + domestic_j = "DOMESTIC_J" + # Standard/Domestic household, type "K", DS 60884-2-D1, 3 pins + domestic_k = "DOMESTIC_K" + # Standard/Domestic household, type "L", CEI 23-16-VII, 3 pins + domestic_l = "DOMESTIC_L" + # IEC 60309-2 Industrial Connector single phase 16 amperes (usually blue) + iec_60309_2_single_16 = "IEC_60309_2_single_16" + # IEC 60309-2 Industrial Connector three phases 16 amperes (usually red) + iec_60309_2_three_16 = "IEC_60309_2_three_16" + # IEC 60309-2 Industrial Connector three phases 32 amperes (usually red) + iec_60309_2_three_32 = "IEC_60309_2_three_32" + # IEC 60309-2 Industrial Connector three phases 64 amperes (usually red) + iec_60309_2_three_64 = "IEC_60309_2_three_64" + # IEC 62196 Type 1 "SAE J1772" + iec_62196_t1 = "IEC_62196_T1" + # Combo Type 1 based, DC + iec_62196_t1_combo = "IEC_62196_T1_COMBO" + # IEC 62196 Type 2 "Mennekes" + iec_62196_t2 = "IEC_62196_T2" + # Combo Type 2 based, DC + iec_62196_t2_combo = "IEC_62196_T2_COMBO" + # IEC 62196 Type 3A + iec_62196_t3a = "IEC_62196_T3A" + # IEC 62196 Type 3C "Scame" + iec_62196_t3c = "IEC_62196_T3C" + # Tesla Connector "Roadster"-type (round, 4 pin) + tesla_r = "TESLA_R" + # Tesla Connector "Model-S"-type (oval, 5 pin) + tesla_s = "TESLA_S" + + +class PowerType(str, Enum): # noqa + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#418-powertype-enum + """ + + # AC single phase. + ac_1_phase = "AC_1_PHASE" + # AC three phases. + ac_3_phase = "AC_3_PHASE" + # Direct Current. + dc = "DC" diff --git a/py_ocpi/modules/locations/v_2_1_1/schemas.py b/py_ocpi/modules/locations/v_2_1_1/schemas.py new file mode 100644 index 0000000..ad6420b --- /dev/null +++ b/py_ocpi/modules/locations/v_2_1_1/schemas.py @@ -0,0 +1,163 @@ +from typing import List, Optional + +from pydantic import BaseModel + +from py_ocpi.core.data_types import DisplayText, DateTime, String, URL + +from py_ocpi.modules.locations.schemas import ( + AdditionalGeoLocation, + EnergyMix, + GeoLocation, + Hours, + StatusSchedule, +) +from py_ocpi.modules.locations.v_2_1_1.enums import ( + Capability, + ConnectorFormat, + ConnectorType, + Facility, + LocationType, + ParkingRestriction, + PowerType, + ImageCategory, + Status, +) + + +class Image(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#414-image-class + """ + + url: URL + thumbnail: Optional[URL] + category: ImageCategory + type: String(max_length=4) # type: ignore + width: Optional[int] + height: Optional[int] + + +class BusinessDetails(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#41-businessdetails-class + """ + + name: String(max_length=100) # type: ignore + website: Optional[URL] + logo: Optional[Image] + + +class Connector(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#33-connector-object + """ + + id: String(max_length=36) # type: ignore + standard: ConnectorType + format: ConnectorFormat + power_type: PowerType + voltage: int + amperage: int + tariff_id: String(max_length=36) # type: ignore + terms_and_conditions: Optional[URL] + last_updated: DateTime + + +class ConnectorPartialUpdate(BaseModel): + id: Optional[String(max_length=36)] # type: ignore + standard: Optional[ConnectorType] + format: Optional[ConnectorFormat] + power_type: Optional[PowerType] + voltage: Optional[int] + amperage: Optional[int] + tariff_id: Optional[String(max_length=36)] # type: ignore + terms_and_conditions: Optional[URL] + last_updated: Optional[DateTime] + + +class EVSE(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#32-evse-object + """ + + uid: String(max_length=39) # type: ignore + evse_id: Optional[String(max_length=48)] # type: ignore + status: Status + status_schedule: Optional[StatusSchedule] + capabilities: List[Capability] = [] + connectors: List[Connector] + floor_level: Optional[String(max_length=4)] # type: ignore + coordinates: Optional[GeoLocation] + physical_reference: Optional[String(max_length=16)] # type: ignore + directions: List[DisplayText] = [] + parking_restrictions: List[ParkingRestriction] = [] + images: List[Image] = [] + last_updated: DateTime + + +class EVSEPartialUpdate(BaseModel): + uid: Optional[String(max_length=39)] # type: ignore + evse_id: Optional[String(max_length=48)] # type: ignore + status: Optional[Status] + status_schedule: Optional[StatusSchedule] + capabilities: Optional[List[Capability]] + connectors: Optional[List[Connector]] + floor_level: Optional[String(max_length=4)] # type: ignore + coordinates: Optional[GeoLocation] + physical_reference: Optional[String(max_length=16)] # type: ignore + directions: Optional[List[DisplayText]] + parking_restrictions: Optional[List[ParkingRestriction]] + images: Optional[List[Image]] + last_updated: Optional[DateTime] + + +class Location(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_locations.md#31-location-object + """ + + id: String(max_length=39) # type: ignore + type: LocationType + name: Optional[String(max_length=255)] # type: ignore + address: String(max_length=45) # type: ignore + city: String(max_length=45) # type: ignore + postal_code: Optional[String(max_length=10)] # type: ignore + country: String(max_length=3) # type: ignore + coordinates: GeoLocation + related_locations: List[AdditionalGeoLocation] = [] + evses: List[EVSE] = [] + directions: List[DisplayText] = [] + operator: Optional[BusinessDetails] + suboperator: Optional[BusinessDetails] + owner: Optional[BusinessDetails] + facilities: List[Facility] = [] + time_zone: String(max_length=255) # type: ignore + opening_times: Optional[Hours] + charging_when_closed: Optional[bool] + images: List[Image] = [] + energy_mix: Optional[EnergyMix] + last_updated: DateTime + + +class LocationPartialUpdate(BaseModel): + id: Optional[String(max_length=39)] # type: ignore + type: Optional[LocationType] + name: Optional[String(max_length=255)] # type: ignore + address: Optional[String(max_length=45)] # type: ignore + city: Optional[String(max_length=45)] # type: ignore + postal_code: Optional[String(max_length=10)] # type: ignore + country: Optional[String(max_length=3)] # type: ignore + coordinates: Optional[GeoLocation] + related_locations: Optional[List[AdditionalGeoLocation]] + evses: Optional[List[EVSE]] + directions: Optional[List[DisplayText]] + operator: Optional[BusinessDetails] + suboperator: Optional[BusinessDetails] + owner: Optional[BusinessDetails] + facilities: Optional[List[Facility]] + time_zone: Optional[String(max_length=255)] # type: ignore + opening_times: Optional[Hours] + charging_when_closed: Optional[bool] + images: Optional[List[Image]] + energy_mix: Optional[EnergyMix] + last_updated: Optional[DateTime] diff --git a/py_ocpi/modules/locations/v_2_2_1/api/cpo.py b/py_ocpi/modules/locations/v_2_2_1/api/cpo.py index 169b20b..1e1ff80 100644 --- a/py_ocpi/modules/locations/v_2_2_1/api/cpo.py +++ b/py_ocpi/modules/locations/v_2_2_1/api/cpo.py @@ -5,30 +5,61 @@ from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters router = APIRouter( - prefix='/locations', + prefix="/locations", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) @router.get("/", response_model=OCPIResponse) -async def get_locations(request: Request, - response: Response, - crud: Crud = Depends(get_crud), - adapter: Adapter = Depends(get_adapter), - filters: dict = Depends(pagination_filters)): +async def get_locations( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get locations. + + Retrieves a list of locations based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return Locations that have + last_updated after this Date/Time (default=None). + - date_to (datetime): Only return Locations that have + last_updated before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of locations. + """ + logger.info("Received request to get locations.") auth_token = get_auth_token(request) - data_list = await get_list(response, filters, ModuleID.locations, RoleEnum.cpo, - VersionNumber.v_2_2_1, crud, auth_token=auth_token) + data_list = await get_list( + response, + filters, + ModuleID.locations, + RoleEnum.cpo, + VersionNumber.v_2_2_1, + crud, + auth_token=auth_token, + ) locations = [] for data in data_list: locations.append(adapter.location_adapter(data).dict()) + logger.debug(f"Amount of locations in response: {len(locations)}") return OCPIResponse( data=locations, **status.OCPI_1000_GENERIC_SUCESS_CODE, @@ -36,47 +67,154 @@ async def get_locations(request: Request, @router.get("/{location_id}", response_model=OCPIResponse) -async def get_location(request: Request, location_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def get_location( + request: Request, + location_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get location by ID. + + Retrieves location details based on the specified ID. + + **Path parameters:** + - location_id (str): The ID of the location to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the location details. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID is not found. + """ + logger.info(f"Received request to get location by id - `{location_id}`.") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.locations, RoleEnum.cpo, location_id, auth_token=auth_token, - version=VersionNumber.v_2_2_1) - return OCPIResponse( - data=[adapter.location_adapter(data).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + data = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + location_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, ) + if data: + return OCPIResponse( + data=[adapter.location_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError @router.get("/{location_id}/{evse_uid}", response_model=OCPIResponse) -async def get_evse(request: Request, location_id: CiString(36), evse_uid: CiString(48), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def get_evse( + request: Request, + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get EVSE by ID. + + Retrieves Electric Vehicle Supply Equipment (EVSE) details + based on the specified Location ID and EVSE UID. + + **Path parameters:** + - location_id (str): The ID of the location containing + the EVSE (36 characters). + - evse_uid (str): The UID of the EVSE to retrieve (48 characters). + + **Returns:** + The OCPIResponse containing the EVSE details. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID + or EVSE with the specified UID is not found. + """ + logger.info( + f"Received request to get evse by id - `{location_id}` (location id - `{evse_uid}`)" + ) auth_token = get_auth_token(request) - data = await crud.get(ModuleID.locations, RoleEnum.cpo, location_id, auth_token=auth_token, - version=VersionNumber.v_2_2_1) - location = adapter.location_adapter(data) - for evse in location.evses: - if evse.uid == evse_uid: - return OCPIResponse( - data=[evse.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, - ) + data = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + location_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + if data: + location = adapter.location_adapter(data) + for evse in location.evses: + if evse.uid == evse_uid: + return OCPIResponse( + data=[evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + +@router.get( + "/{location_id}/{evse_uid}/{connector_id}", response_model=OCPIResponse +) +async def get_connector( + request: Request, + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + connector_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Connector by ID. -@router.get("/{location_id}/{evse_uid}/{connector_id}", response_model=OCPIResponse) -async def get_connector(request: Request, location_id: CiString(36), evse_uid: CiString(48), connector_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + Retrieves Connector details based on the specified Location ID, + EVSE UID, and Connector ID. + + **Path parameters:** + - location_id (str): The ID of the location containing + the EVSE (36 characters). + - evse_uid (str): The UID of the EVSE to retrieve (48 characters). + - connector_id (str): The ID of the connector + to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the Connector details. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID, + EVSE with the specified UID, or Connector with + the specified ID is not found. + """ + logger.info( + f"Received request to get connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) auth_token = get_auth_token(request) - data = await crud.get(ModuleID.locations, RoleEnum.cpo, location_id, auth_token=auth_token, - version=VersionNumber.v_2_2_1) - location = adapter.location_adapter(data) - for evse in location.evses: - if evse.uid == evse_uid: - for connector in evse.connectors: - if connector.id == connector_id: - return OCPIResponse( - data=[connector.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, - ) + data = await crud.get( + ModuleID.locations, + RoleEnum.cpo, + location_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) + if data: + location = adapter.location_adapter(data) + for evse in location.evses: + if evse.uid == evse_uid: + for connector in evse.connectors: + if connector.id == connector_id: + return OCPIResponse( + data=[connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug( + f"Connector with id `{connector_id}` was not found." + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/locations/v_2_2_1/api/emsp.py b/py_ocpi/modules/locations/v_2_2_1/api/emsp.py index 2e84c4a..23606ee 100644 --- a/py_ocpi/modules/locations/v_2_2_1/api/emsp.py +++ b/py_ocpi/modules/locations/v_2_2_1/api/emsp.py @@ -1,89 +1,282 @@ +import copy + from fastapi import APIRouter, Depends, Request from py_ocpi.core.utils import get_auth_token, partially_update_attributes from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError from py_ocpi.core.dependencies import get_crud, get_adapter from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.modules.locations.v_2_2_1.schemas import ( - Location, LocationPartialUpdate, - EVSE, EVSEPartialUpdate, - Connector, ConnectorPartialUpdate, + Location, + LocationPartialUpdate, + EVSE, + EVSEPartialUpdate, + Connector, + ConnectorPartialUpdate, ) router = APIRouter( - prefix='/locations', + prefix="/locations", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) -@router.get("/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse) -async def get_location(request: Request, country_code: CiString(2), party_id: CiString(3), location_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.get( + "/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse +) +async def get_location( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Location. + + Retrieves a location based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str: The three-letter party ID. + - location_id (str): The ID of the location to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the location data. + + **Raises:** + NotFoundOCPIError: NotFoundOCPIError: If the location is not found. + """ + logger.info( + f"Received request to get location with id - `{location_id}`." + ) auth_token = get_auth_token(request) - data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - return OCPIResponse( - data=[adapter.location_adapter(data).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) + if data: + return OCPIResponse( + data=[adapter.location_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError -@router.get("/{country_code}/{party_id}/{location_id}/{evse_uid}", response_model=OCPIResponse) -async def get_evse(request: Request, country_code: CiString(2), party_id: CiString(3), location_id: CiString(36), - evse_uid: CiString(48), crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.get( + "/{country_code}/{party_id}/{location_id}/{evse_uid}", + response_model=OCPIResponse, +) +async def get_evse( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get EVSE. + + Retrieves an EVSE based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location containing + the EVSE (36 characters). + - evse_uid (str): The UID of the EVSE to retrieve (48 characters). + + **Returns:** + The OCPIResponse containing the EVSE data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID + or EVSE with the specified UID is not found. + """ + logger.info( + f"Received request to get evse by id - `{location_id}` (location id - `{evse_uid}`)" + ) auth_token = get_auth_token(request) - data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - location = adapter.location_adapter(data) - for evse in location.evses: - if evse.uid == evse_uid: - return OCPIResponse( - data=[evse.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, - ) - - -@router.get("/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", response_model=OCPIResponse) -async def get_connector(request: Request, country_code: CiString(2), party_id: CiString(3), location_id: CiString(36), - evse_uid: CiString(48), connector_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + if data: + location = adapter.location_adapter(data) + for evse in location.evses: + if evse.uid == evse_uid: + return OCPIResponse( + data=[evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.get( + "/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", + response_model=OCPIResponse, +) +async def get_connector( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + connector_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Connector. + + Retrieves a connector based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location containing + the EVSE (36 characters). + - evse_uid (str): The UID of the EVSE containing + the connector (48 characters). + - connector_id (str): The ID of the connector + to retrieve (36 characters). + + **Returns:** + The OCPIResponse containing the connector data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID, EVSE with the + specified UID, or Connector with the specified ID is not found. + """ + logger.info( + f"Received request to get connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) auth_token = get_auth_token(request) - data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - location = adapter.location_adapter(data) - for evse in location.evses: - if evse.uid == evse_uid: - for connector in evse.connectors: - if connector.id == connector_id: - return OCPIResponse( - data=[connector.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, - ) - - -@router.put("/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse) -async def add_or_update_location(request: Request, country_code: CiString(2), party_id: CiString(3), - location_id: CiString(36), location: Location, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + if data: + location = adapter.location_adapter(data) + for evse in location.evses: + if evse.uid == evse_uid: + for connector in evse.connectors: + if connector.id == connector_id: + return OCPIResponse( + data=[connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug( + f"Connector with id `{connector_id}` was not found." + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse +) +async def add_or_update_location( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + location: Location, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Location. + + Adds or updates a location based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location to add + or update (36 characters). + + **Request body:** + location (Location): The location object. + + **Returns:** + The OCPIResponse containing the added or updated location data. + + **Raises:** + NotFoundOCPIError: If the location is not found. + """ + logger.info( + f"Received request to add or update location with id - `{location_id}`." + ) + logger.debug(f"Location data to update - {location.dict()}") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) + data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) if data: - data = await crud.update(ModuleID.locations, RoleEnum.emsp, location.dict(), location_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + logger.debug(f"Update location with id - `{location_id}`.") + data = await crud.update( + ModuleID.locations, + RoleEnum.emsp, + location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) else: - data = await crud.create(ModuleID.locations, RoleEnum.emsp, location.dict(), - auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + logger.debug(f"Create location with id - `{location_id}`.") + data = await crud.create( + ModuleID.locations, + RoleEnum.emsp, + location.dict(), + auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[adapter.location_adapter(data).dict()], @@ -91,149 +284,430 @@ async def add_or_update_location(request: Request, country_code: CiString(2), pa ) -@router.put("/{country_code}/{party_id}/{location_id}/{evse_uid}", response_model=OCPIResponse) -async def add_or_update_evse(request: Request, country_code: CiString(2), party_id: CiString(3), - location_id: CiString(36), evse_uid: CiString(48), evse: EVSE, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.put( + "/{country_code}/{party_id}/{location_id}/{evse_uid}", + response_model=OCPIResponse, +) +async def add_or_update_evse( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + evse: EVSE, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update EVSE. + + Adds or updates an EVSE based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location (36 characters). + - evse_uid (str): The ID of the EVSE to add or update (48 characters). + + **Request body:** + evse (EVSE): The EVSE object. + + **Returns:** + The OCPIResponse containing the added or updated EVSE data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID is not found. + """ + logger.info( + f"Received request to add or update evse by id - `{location_id}` " + f"(location id - `{evse_uid}`)" + ) + logger.debug(f"Evse data to update - {evse.dict()}") auth_token = get_auth_token(request) - old_data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - old_location = adapter.location_adapter(old_data) - - is_new_evse = True - for old_evse in old_location.evses: - if old_evse.uid == evse_uid: - is_new_evse = False - break - new_location = old_location - if not is_new_evse: - new_location.evses.remove(old_evse) - new_location.evses.append(evse) - - await crud.update(ModuleID.locations, RoleEnum.emsp, new_location.dict(), location_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) - - return OCPIResponse( - data=[evse.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) - - -@router.put("/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", response_model=OCPIResponse) -async def add_or_update_connector(request: Request, country_code: CiString(2), party_id: CiString(3), - location_id: CiString(36), evse_uid: CiString(48), - connector_id: CiString(36), connector: Connector, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + if old_data: + old_location = adapter.location_adapter(old_data) + new_location = copy.deepcopy(old_location) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + logger.debug(f"Update evse with id - {evse_uid}") + new_location.evses.remove(old_evse) + break + + new_location.evses.append(evse) + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", + response_model=OCPIResponse, +) +async def add_or_update_connector( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + connector_id: CiString(36), # type: ignore + connector: Connector, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Connector. + + Adds or updates a connector based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location (36 characters). + - evse_uid (str): The ID of the EVSE (48 characters). + - connector_id (str): The ID of the connector to add + or update (36 characters). + + **Request body:** + connector (Connector): The connector object. + + **Returns:** + The OCPIResponse containing the added or updated connector data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID + or EVSE with the specified UID is not found. + """ + logger.info( + f"Received request to add or update connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) + logger.debug(f"Connector data to update - {connector.dict()}") auth_token = get_auth_token(request) - old_data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - old_location = adapter.location_adapter(old_data) - - is_new_connector = True - for old_evse in old_location.evses: - if old_evse.uid == evse_uid: - for old_onnector in old_evse.connectors: - if old_onnector.id == connector_id: - is_new_connector = False - break - break - new_location = old_location - new_location.evses.remove(old_evse) - if not is_new_connector: - old_evse.connectors.remove(old_onnector) - new_evse = old_evse - new_evse.connectors.append(connector) - new_location.evses.append(new_evse) - - await crud.update(ModuleID.locations, RoleEnum.emsp, new_location.dict(), location_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) - - return OCPIResponse( - data=[connector.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) - - -@router.patch("/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse) -async def partial_update_location(request: Request, country_code: CiString(2), party_id: CiString(3), - location_id: CiString(36), location: LocationPartialUpdate, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + if old_data: + old_location = adapter.location_adapter(old_data) + new_location = copy.deepcopy(old_location) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + new_location.evses.remove(old_evse) + new_evse = copy.deepcopy(old_evse) + for old_connector in old_evse.connectors: + if old_connector.id == connector_id: + logger.debug( + f"Update connector with id - {connector_id}" + ) + new_evse.connectors.remove(old_connector) + break + new_evse.connectors.append(connector) + new_location.evses.append(new_evse) + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.patch( + "/{country_code}/{party_id}/{location_id}", response_model=OCPIResponse +) +async def partial_update_location( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + location: LocationPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Location. + + Partially updates a location based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location to partially + update (36 characters). + + **Request body:** + location (LocationPartialUpdate): The partial location update object. + + **Returns:** + The OCPIResponse containing the partially updated location data. + + **Raises:** + NotFoundOCPIError: If the location is not found. + """ + logger.info( + f"Received request to partially update location with id - `{location_id}`." + ) + logger.debug(f"Location data to update - {location.dict()}") auth_token = get_auth_token(request) - old_data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - old_location = adapter.location_adapter(old_data) - - new_location = old_location - partially_update_attributes(new_location, location.dict(exclude_defaults=True, exclude_unset=True)) - - data = await crud.update(ModuleID.locations, RoleEnum.emsp, new_location.dict(), location_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) - - return OCPIResponse( - data=[adapter.location_adapter(data).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) - - -@router.patch("/{country_code}/{party_id}/{location_id}/{evse_uid}", response_model=OCPIResponse) -async def partial_update_evse(request: Request, country_code: CiString(2), party_id: CiString(3), - location_id: CiString(36), evse_uid: CiString(48), evse: EVSEPartialUpdate, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + if old_data: + old_location = adapter.location_adapter(old_data) + new_location = copy.deepcopy(old_location) + + partially_update_attributes( + new_location, + location.dict(exclude_defaults=True, exclude_unset=True), + ) + + data = await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[adapter.location_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.patch( + "/{country_code}/{party_id}/{location_id}/{evse_uid}", + response_model=OCPIResponse, +) +async def partial_update_evse( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + evse: EVSEPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update EVSE. + + Partially updates an EVSE based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location to partially + update (36 characters). + - evse_uid (str): The UID of the EVSE + to partially update (48 characters). + + **Request body:** + evse (EVSEPartialUpdate): The partial EVSE update object. + + **Returns:** + The OCPIResponse containing the partially updated EVSE data. + + **Raises:** + NotFoundOCPIError: If the location with the specified ID + or EVSE with the specified UID is not found. + """ + logger.info( + f"Received request to partially update evse by id - `{location_id}` " + f"(location id - `{evse_uid}`)" + ) + logger.debug(f"Evse data to update - {evse.dict()}") auth_token = get_auth_token(request) - old_data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - old_location = adapter.location_adapter(old_data) - - for old_evse in old_location.evses: - if old_evse.uid == evse_uid: - break - new_evse = old_evse - partially_update_attributes(new_evse, evse.dict(exclude_defaults=True, exclude_unset=True)) - new_location = old_location - - await crud.update(ModuleID.locations, RoleEnum.emsp, new_location.dict(), location_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) - - return OCPIResponse( - data=[new_evse.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) - - -@router.patch("/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", response_model=OCPIResponse) -async def partial_update_connector(request: Request, country_code: CiString(2), party_id: CiString(3), - location_id: CiString(36), evse_uid: CiString(48), - connector_id: CiString(36), connector: ConnectorPartialUpdate, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + if old_data: + old_location = adapter.location_adapter(old_data) + new_location = copy.deepcopy(old_location) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + new_location.evses.remove(old_evse) + new_evse = copy.deepcopy(old_evse) + partially_update_attributes( + new_evse, + evse.dict(exclude_defaults=True, exclude_unset=True), + ) + new_location.evses.append(new_evse) + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + return OCPIResponse( + data=[new_evse.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError + + +@router.patch( + "/{country_code}/{party_id}/{location_id}/{evse_uid}/{connector_id}", + response_model=OCPIResponse, +) +async def partial_update_connector( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + location_id: CiString(36), # type: ignore + evse_uid: CiString(48), # type: ignore + connector_id: CiString(36), # type: ignore + connector: ConnectorPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Connector. + + Partially updates a connector based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - location_id (str): The ID of the location + to partially update (36 characters). + - evse_uid (str): The UID of the EVSE + to partially update (48 characters). + - connector_id (str): The ID of the connector + to partially update (36 characters). + + **Request body:** + connector (ConnectorPartialUpdate): The partial connector update object. + + **Returns:** + The OCPIResponse containing the partially updated connector data. + + **Raises:** + NotFoundOCPIError:If the location with the specified ID, EVSE with + the specified UID, or Connector with the specified ID is not found. + """ + logger.info( + f"Received request to partially update connector by id - `{connector_id}` " + f"(location id - `{location_id}`, evse id - `{evse_uid}`)" + ) + logger.debug(f"Connector data to update - {connector.dict()}") auth_token = get_auth_token(request) - old_data = await crud.get(ModuleID.locations, RoleEnum.emsp, location_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - old_location = adapter.location_adapter(old_data) - - for old_evse in old_location.evses: - if old_evse.uid == evse_uid: - for old_onnector in old_evse.connectors: - if old_onnector.id == connector_id: - break - break - new_connector = old_onnector - partially_update_attributes(new_connector, connector.dict(exclude_defaults=True, exclude_unset=True)) - new_location = old_location - - await crud.update(ModuleID.locations, RoleEnum.emsp, new_location.dict(), location_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) - - return OCPIResponse( - data=[new_connector.dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + old_data = await crud.get( + ModuleID.locations, + RoleEnum.emsp, + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) + if old_data: + old_location = adapter.location_adapter(old_data) + + for old_evse in old_location.evses: + if old_evse.uid == evse_uid: + for old_connector in old_evse.connectors: + if old_connector.id == connector_id: + new_connector = old_connector + partially_update_attributes( + new_connector, + connector.dict( + exclude_defaults=True, exclude_unset=True + ), + ) + new_location = old_location + + await crud.update( + ModuleID.locations, + RoleEnum.emsp, + new_location.dict(), + location_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[new_connector.dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug( + f"Connector with id `{connector_id}` was not found." + ) + logger.debug(f"Evse with id `{evse_uid}` was not found.") + logger.debug(f"Location with id `{location_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/locations/v_2_2_1/enums.py b/py_ocpi/modules/locations/v_2_2_1/enums.py index 7d66cc3..83eefe6 100644 --- a/py_ocpi/modules/locations/v_2_2_1/enums.py +++ b/py_ocpi/modules/locations/v_2_2_1/enums.py @@ -1,306 +1,209 @@ -from enum import Enum +from py_ocpi.modules.locations.enums import * # noqa -class ParkingRestriction(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1417-parkingrestriction-enum - """ - # Reserved parking spot for electric vehicles. - ev_only = 'EV_ONLY' - # Parking is only allowed while plugged in (charging). - plugged = 'PLUGGED' - # Reserved parking spot for disabled people with valid ID. - disables = 'DISABLED' - # Parking spot for customers/guests only, for example in case of a hotel or shop. - customers = 'CUSTOMERS' - # Parking spot only suitable for (electric) motorcycles or scooters. - motorcycle = 'MOTORCYCLES' - - -class ParkingType(str, Enum): +class ParkingType(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1418-parkingtype-enum """ - # Location on a parking facility/rest area along a motorway, freeway, interstate, highway etc. - along_motorway = 'ALONG_MOTORWAY' + # Location on a parking facility/rest area + # along a motorway, freeway, interstate, highway etc. + along_motorway = "ALONG_MOTORWAY" # Multistorey car park. - parking_garage = 'PARKING_GARAGE' - # A cleared area that is intended for parking vehicles, i.e. at super markets, bars, etc. - parking_lot = 'PARKING_LOT' + parking_garage = "PARKING_GARAGE" + # A cleared area that is intended + # for parking vehicles, i.e. at super markets, bars, etc. + parking_lot = "PARKING_LOT" # Location is on the driveway of a house/building. - on_driveway = 'ON_DRIVEWAY' + on_driveway = "ON_DRIVEWAY" # Parking in public space along a street. - on_street = 'ON_STREET' + on_street = "ON_STREET" # Multistorey car park, mainly underground. - underground_garage = 'UNDERGROUND_GARAGE' + underground_garage = "UNDERGROUND_GARAGE" -class Facility(str, Enum): +class Facility(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1412-facility-enum """ # A hotel. - hotel = 'HOTEL' + hotel = "HOTEL" # A restaurant. - restaurant = 'RESTAURANT' + restaurant = "RESTAURANT" # A cafe. - cafe = 'CAFE' + cafe = "CAFE" # A mall or shopping center. - mall = 'MALL' + mall = "MALL" # A supermarket. - supermarket = 'SUPERMARKET' + supermarket = "SUPERMARKET" # Sport facilities: gym, field etc. - sport = 'SPORT' + sport = "SPORT" # A recreation area. - recreation_area = 'RECREATION_AREA' + recreation_area = "RECREATION_AREA" # Located in, or close to, a park, nature reserve etc. - nature = 'NATURE' + nature = "NATURE" # A museum. - museum = 'MUSEUM' + museum = "MUSEUM" # A bike/e-bike/e-scooter sharing location. - bike_sharing = 'BIKE_SHARING' + bike_sharing = "BIKE_SHARING" # A bus stop. - bus_stop = 'BUS_STOP' + bus_stop = "BUS_STOP" # A taxi stand. - taxi_stand = 'TAXI_STAND' + taxi_stand = "TAXI_STAND" # A tram stop/station. - tram_shop = 'TRAM_STOP' + tram_shop = "TRAM_STOP" # A metro station. - metro_station = 'METRO_STATION' + metro_station = "METRO_STATION" # A train station. - train_station = 'TRAIN_STATION' + train_station = "TRAIN_STATION" # An airport. - airport = 'AIRPORT' + airport = "AIRPORT" # A parking lot. - parking_lot = 'PARKING_LOT' + parking_lot = "PARKING_LOT" # A carpool parking. - carpool_parking = 'CARPOOL_PARKING' + carpool_parking = "CARPOOL_PARKING" # A Fuel station. - fuel_station = 'FUEL_STATION' + fuel_station = "FUEL_STATION" # Wifi or other type of internet available. - wifi = 'WIFI' + wifi = "WIFI" -class Status(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1422-status-enum - """ - # The EVSE/Connector is able to start a new charging session. - available = 'AVAILABLE' - # The EVSE/Connector is not accessible because of a physical barrier, i.e. a car. - blocked = 'BLOCKED' - # The EVSE/Connector is in use. - charging = 'CHARGING' - # The EVSE/Connector is not yet active, or temporarily not available for use, but not broken or defect. - inoperative = 'INOPERATIVE' - # The EVSE/Connector is currently out of order, some part/components may be broken/defect. - outoforder = 'OUTOFORDER' - # The EVSE/Connector is planned, will be operating soon. - planned = 'PLANNED' - # The EVSE/Connector was discontinued/removed. - removed = 'REMOVED' - # The EVSE/Connector is reserved for a particular EV driver and is unavailable for other drivers. - reserved = 'RESERVED' - # No status information available (also used when offline). - unknown = 'UNKNOWN' - - -class Capability(str, Enum): +class Capability(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#143-capability-enum """ # The EVSE supports charging profiles. - charging_profile_capable = 'CHARGING_PROFILE_CAPABLE' + charging_profile_capable = "CHARGING_PROFILE_CAPABLE" # The EVSE supports charging preferences. - charging_preferences_capable = 'CHARGING_PREFERENCES_CAPABLE' + charging_preferences_capable = "CHARGING_PREFERENCES_CAPABLE" # EVSE has a payment terminal that supports chip cards. - chip_card_support = 'CHIP_CARD_SUPPORT' + chip_card_support = "CHIP_CARD_SUPPORT" # EVSE has a payment terminal that supports contactless cards. - contactless_card_support = 'CONTACTLESS_CARD_SUPPORT' - # EVSE has a payment terminal that makes it possible to pay for charging using a credit card. - credit_card_payable = 'CREDIT_CARD_PAYABLE' - # EVSE has a payment terminal that makes it possible to pay for charging using a debit card. - debit_card_payable = 'DEBIT_CARD_PAYABLE' + contactless_card_support = "CONTACTLESS_CARD_SUPPORT" + # EVSE has a payment terminal + # that makes it possible to pay for charging using a credit card. + credit_card_payable = "CREDIT_CARD_PAYABLE" + # EVSE has a payment terminal + # that makes it possible to pay for charging using a debit card. + debit_card_payable = "DEBIT_CARD_PAYABLE" # EVSE has a payment terminal with a pin-code entry device. - ped_terminal = 'PED_TERMINAL' + ped_terminal = "PED_TERMINAL" # The EVSE can remotely be started/stopped. - remote_start_stop_capable = 'REMOTE_START_STOP_CAPABLE' + remote_start_stop_capable = "REMOTE_START_STOP_CAPABLE" # The EVSE can be reserved. - reservable = 'RESERVABLE' + reservable = "RESERVABLE" # Charging at this EVSE can be authorized with an RFID token. - rfid_reader = 'RFID_READER' + rfid_reader = "RFID_READER" # When a StartSession is sent to this EVSE, the MSP is required to add # the optional connector_id field in the StartSession object. - start_session_connector_required = 'START_SESSION_CONNECTOR_REQUIRED' + start_session_connector_required = "START_SESSION_CONNECTOR_REQUIRED" # This EVSE supports token groups, two or more tokens work as one, # so that a session can be started with one token and stopped with another # (handy when a card and key-fob are given to the EV-driver). - token_group_capable = 'TOKEN_GROUP_CAPABLE' # nosec - # Connectors have mechanical lock that can be requested by the eMSP to be unlocked. - unlook_capable = 'UNLOCK_CAPABLE' + token_group_capable = "TOKEN_GROUP_CAPABLE" # nosec + # Connectors have mechanical lock that + # can be requested by the eMSP to be unlocked. + unlook_capable = "UNLOCK_CAPABLE" -class ConnectorType(str, Enum): +class ConnectorType(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#145-connectortype-enum """ # The connector type is CHAdeMO, DC - chadmeo = 'CHADEMO' - # The ChaoJi connector. The new generation charging connector, harmonized between CHAdeMO and GB/T. DC. - chaoji = 'CHAOJI' + chademo = "CHADEMO" + # The ChaoJi connector. The new generation charging connector, + # harmonized between CHAdeMO and GB/T. DC. + chaoji = "CHAOJI" # Standard/Domestic household, type "A", NEMA 1-15, 2 pins - domestic_a = 'DOMESTIC_A' + domestic_a = "DOMESTIC_A" # Standard/Domestic household, type "B", NEMA 5-15, 3 pins - domestic_b = 'DOMESTIC_B' + domestic_b = "DOMESTIC_B" # Standard/Domestic household, type "C", CEE 7/17, 2 pins - domestic_c = 'DOMESTIC_C' + domestic_c = "DOMESTIC_C" # Standard/Domestic household, type "D", 3 pin - domestic_d = 'DOMESTIC_D' + domestic_d = "DOMESTIC_D" # Standard/Domestic household, type "E", CEE 7/5 3 pins - domestic_e = 'DOMESTIC_E' + domestic_e = "DOMESTIC_E" # Standard/Domestic household, type "F", CEE 7/4, Schuko, 3 pins - domestic_f = 'DOMESTIC_F' + domestic_f = "DOMESTIC_F" # Standard/Domestic household, type "G", BS 1363, Commonwealth, 3 pins - domestic_g = 'DOMESTIC_G' + domestic_g = "DOMESTIC_G" # Standard/Domestic household, type "H", SI-32, 3 pins - domestic_h = 'DOMESTIC_H' + domestic_h = "DOMESTIC_H" # Standard/Domestic household, type "I", AS 3112, 3 pins - domestic_i = 'DOMESTIC_I' + domestic_i = "DOMESTIC_I" # Standard/Domestic household, type "J", SEV 1011, 3 pins - domestic_j = 'DOMESTIC_J' + domestic_j = "DOMESTIC_J" # Standard/Domestic household, type "K", DS 60884-2-D1, 3 pins - domestic_k = 'DOMESTIC_K' + domestic_k = "DOMESTIC_K" # Standard/Domestic household, type "L", CEI 23-16-VII, 3 pins - domestic_l = 'DOMESTIC_L' + domestic_l = "DOMESTIC_L" # Standard/Domestic household, type "M", BS 546, 3 pins - domestic_m = 'DOMESTIC_M' + domestic_m = "DOMESTIC_M" # Standard/Domestic household, type "N", NBR 14136, 3 pins - domestic_n = 'DOMESTIC_N' + domestic_n = "DOMESTIC_N" # Standard/Domestic household, type "O", TIS 166-2549, 3 pins - domestic_o = 'DOMESTIC_O' + domestic_o = "DOMESTIC_O" # Guobiao GB/T 20234.2 AC socket/connector - gbt_ac = 'GBT_AC' + gbt_ac = "GBT_AC" # Guobiao GB/T 20234.3 DC connector - gbt_dc = 'GBT_DC' + gbt_dc = "GBT_DC" # IEC 60309-2 Industrial Connector single phase 16 amperes (usually blue) - iec_60309_2_single_16 = 'IEC_60309_2_single_16' + iec_60309_2_single_16 = "IEC_60309_2_single_16" # IEC 60309-2 Industrial Connector three phases 16 amperes (usually red) - iec_60309_2_three_16 = 'IEC_60309_2_three_16' + iec_60309_2_three_16 = "IEC_60309_2_three_16" # IEC 60309-2 Industrial Connector three phases 32 amperes (usually red) - iec_60309_2_three_32 = 'IEC_60309_2_three_32' + iec_60309_2_three_32 = "IEC_60309_2_three_32" # IEC 60309-2 Industrial Connector three phases 64 amperes (usually red) - iec_60309_2_three_64 = 'IEC_60309_2_three_64' + iec_60309_2_three_64 = "IEC_60309_2_three_64" # IEC 62196 Type 1 "SAE J1772" - iec_62196_t1 = 'IEC_62196_T1' + iec_62196_t1 = "IEC_62196_T1" # Combo Type 1 based, DC - iec_62196_t1_combo = 'IEC_62196_T1_COMBO' + iec_62196_t1_combo = "IEC_62196_T1_COMBO" # IEC 62196 Type 2 "Mennekes" - iec_62196_t2 = 'IEC_62196_T2' + iec_62196_t2 = "IEC_62196_T2" # Combo Type 2 based, DC - iec_62196_t2_combo = 'IEC_62196_T2_COMBO' + iec_62196_t2_combo = "IEC_62196_T2_COMBO" # IEC 62196 Type 3A - iec_62196_t3a = 'IEC_62196_T3A' + iec_62196_t3a = "IEC_62196_T3A" # IEC 62196 Type 3C "Scame" - iec_62196_t3c = 'IEC_62196_T3C' + iec_62196_t3c = "IEC_62196_T3C" # NEMA 5-20, 3 pins - nema_5_20 = 'NEMA_5_20' + nema_5_20 = "NEMA_5_20" # NEMA 6-30, 3 pins - nema_6_30 = 'NEMA_6_30' + nema_6_30 = "NEMA_6_30" # NEMA 6-50, 3 pins - nema_6_50 = 'NEMA_6_50' + nema_6_50 = "NEMA_6_50" # NEMA 10-30, 3 pins - nema_10_30 = 'NEMA_10_30' + nema_10_30 = "NEMA_10_30" # NEMA 10-50, 3 pins - nema_10_50 = 'NEMA_10_50' + nema_10_50 = "NEMA_10_50" # NEMA 14-30, 3 pins, rating of 30 A - nema_14_30 = 'NEMA_14_30' + nema_14_30 = "NEMA_14_30" # NEMA 14-50, 3 pins, rating of 50 A - nema_14_50 = 'NEMA_14_50' + nema_14_50 = "NEMA_14_50" # On-board Bottom-up-Pantograph typically for bus charging - pantograph_bottom_up = 'PANTOGRAPH_BOTTOM_UP' + pantograph_bottom_up = "PANTOGRAPH_BOTTOM_UP" # Off-board Top-down-Pantograph typically for bus charging - pantograph_top_down = 'PANTOGRAPH_TOP_DOWN' + pantograph_top_down = "PANTOGRAPH_TOP_DOWN" # Tesla Connector "Roadster"-type (round, 4 pin) - tesla_r = 'TESLA_R' + tesla_r = "TESLA_R" # Tesla Connector "Model-S"-type (oval, 5 pin) - tesla_s = 'TESLA_S' - - -class ConnectorFormat(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#144-connectorformat-enum - """ - # The connector is a socket; the EV user needs to bring a fitting plug. - socket = 'SOCKET' - # The connector is an attached cable; the EV users car needs to have a fitting inlet. - cable = 'CABLE' + tesla_s = "TESLA_S" -class PowerType(str, Enum): +class PowerType(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1419-powertype-enum """ + # AC single phase. - ac_1_phase = 'AC_1_PHASE' + ac_1_phase = "AC_1_PHASE" # AC two phases, only two of the three available phases connected. - ac_2_phase = 'AC_2_PHASE' + ac_2_phase = "AC_2_PHASE" # AC two phases using split phase system. - ac_2_phase_split = 'AC_2_PHASE_SPLIT' + ac_2_phase_split = "AC_2_PHASE_SPLIT" # AC three phases. - ac_3_phase = 'AC_3_PHASE' + ac_3_phase = "AC_3_PHASE" # Direct Current. - dc = 'DC' - - -class ImageCategory(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1416-imagecategory-enum - """ - # Photo of the physical device that contains one or more EVSEs. - charger = 'CHARGER' - # Location entrance photo. Should show the car entrance to the location from street side. - entrance = 'ENTRANCE' - # Location overview photo. - location = 'LOCATION' - # Logo of an associated roaming network to be displayed with the EVSE for example in lists, - # maps and detailed information views. - network = 'NETWORK' - # Logo of the charge point operator, for example a municipality, - # to be displayed in the EVSEs detailed information view or in lists and maps, if no network logo is present. - operator = 'OPERATOR' - # Other - other = 'OTHER' - # Logo of the charge point owner, for example a local store, to be displayed in the EVSEs detailed information view. - owner = 'OWNER' - - -class EnergySourceCategory(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#148-energysourcecategory-enum - """ - # Nuclear power sources. - nuclear = 'NUCLEAR' - # All kinds of fossil power sources. - general_fossil = 'GENERAL_FOSSIL' - # Fossil power from coal. - coal = 'COAL' - # Fossil power from gas. - gas = 'GAS' - # All kinds of regenerative power sources. - general_green = 'GENERAL_GREEN' - # Regenerative power from PV. - solar = 'SOLAR' - # Regenerative power from wind turbines. - wind = 'WIND' - # Regenerative power from water turbines. - water = 'WATER' - - -class EnvironmentalImpactCategory(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1410-environmentalimpactcategory-enum - """ - # Produced nuclear waste in grams per kilowatthour. - nuclear_waste = 'NUCLEAR_WASTE' - # Exhausted carbon dioxide in grams per kilowatthour. - carbon_dioxide = 'CARBON_DIOXIDE' + dc = "DC" diff --git a/py_ocpi/modules/locations/v_2_2_1/schemas.py b/py_ocpi/modules/locations/v_2_2_1/schemas.py index 7d04291..452e1ee 100644 --- a/py_ocpi/modules/locations/v_2_2_1/schemas.py +++ b/py_ocpi/modules/locations/v_2_2_1/schemas.py @@ -3,10 +3,30 @@ from py_ocpi.modules.tokens.v_2_2_1.enums import TokenType from py_ocpi.modules.locations.v_2_2_1.enums import ( - EnergySourceCategory, ParkingType, ParkingRestriction, Facility, Status, Capability, - ConnectorFormat, ConnectorType, PowerType, ImageCategory, EnvironmentalImpactCategory + ParkingType, + ParkingRestriction, + Facility, + Status, + Capability, + ConnectorFormat, + ConnectorType, + PowerType, + ImageCategory, +) +from py_ocpi.modules.locations.schemas import ( + AdditionalGeoLocation, + EnergyMix, + GeoLocation, + Hours, + StatusSchedule, +) +from py_ocpi.core.data_types import ( + URL, + CiString, + DisplayText, + String, + DateTime, ) -from py_ocpi.core.data_types import URL, CiString, DisplayText, Number, String, DateTime class PublishTokenType(BaseModel): @@ -32,21 +52,6 @@ class Image(BaseModel): height: Optional[int] -class GeoLocation(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_geolocation_class - """ - latitude: String(max_length=10) - longitude: String(max_length=11) - - -class AdditionalGeoLocation(GeoLocation): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_additionalgeolocation_class - """ - name: Optional[DisplayText] - - class Connector(BaseModel): """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#133-connector-object @@ -76,15 +81,6 @@ class ConnectorPartialUpdate(BaseModel): last_updated: Optional[DateTime] -class StatusSchedule(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1423-statusschedule-class - """ - period_begin: DateTime - period_end: Optional[DateTime] - status: Status - - class EVSE(BaseModel): """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_evse_object @@ -129,60 +125,6 @@ class BusinessDetails(BaseModel): logo: Optional[Image] -class RegularHours(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1421-regularhours-class - """ - weekday: int - period_begin: String(max_length=5) - period_end: String(max_length=5) - - -class ExceptionalPeriod(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#1411-exceptionalperiod-class - """ - period_begin: DateTime - period_end: DateTime - - -class Hours(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_hours_class - """ - twentyfourseven: bool - regular_hours: List[RegularHours] - exceptional_openings: List[ExceptionalPeriod] = [] - exceptional_closings: List[ExceptionalPeriod] = [] - - -class EnergySource(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#147-energysource-class - """ - source: EnergySourceCategory - percentage: Number - - -class EnvironmentalImpact(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#149-environmentalimpact-class - """ - category: EnvironmentalImpactCategory - amount: Number - - -class EnergyMix(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#mod_locations_energymix_class - """ - is_green_energy: bool - energy_sources: List[EnergySource] - environ_impact: Optional[EnvironmentalImpact] - supplier_name: String(max_length=64) - energy_product_name: String(max_length=64) - - class Location(BaseModel): """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_locations.asciidoc#131-location-object diff --git a/py_ocpi/modules/sessions/v_2_1_1/api/__init__.py b/py_ocpi/modules/sessions/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/sessions/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/sessions/v_2_1_1/api/cpo.py b/py_ocpi/modules/sessions/v_2_1_1/api/cpo.py new file mode 100644 index 0000000..bc8ee88 --- /dev/null +++ b/py_ocpi/modules/sessions/v_2_1_1/api/cpo.py @@ -0,0 +1,66 @@ +from fastapi import APIRouter, Depends, Response, Request + +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.utils import get_list, get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters + +router = APIRouter( + prefix="/sessions", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get("/", response_model=OCPIResponse) +async def get_sessions( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get sessions. + + Retrieves a list of sessions based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return Sessions that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return Sessions that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of CDRs. + """ + logger.info("Received request to get sessions.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data_list = await get_list( + response, + filters, + ModuleID.sessions, + RoleEnum.cpo, + VersionNumber.v_2_1_1, + crud, + auth_token=auth_token, + ) + + sessions = [] + for data in data_list: + sessions.append( + adapter.session_adapter(data, VersionNumber.v_2_1_1).dict() + ) + logger.debug(f"Amount of sessions in response: {len(sessions)}") + return OCPIResponse( + data=sessions, + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/sessions/v_2_1_1/api/emsp.py b/py_ocpi/modules/sessions/v_2_1_1/api/emsp.py new file mode 100644 index 0000000..6c42b4f --- /dev/null +++ b/py_ocpi/modules/sessions/v_2_1_1/api/emsp.py @@ -0,0 +1,222 @@ +from copy import deepcopy + +from fastapi import APIRouter, Depends, Request + +from py_ocpi.core import status +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +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.exceptions import NotFoundOCPIError +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.utils import ( + get_auth_token, + partially_update_attributes, +) +from py_ocpi.modules.sessions.v_2_1_1.schemas import ( + SessionPartialUpdate, + Session, +) +from py_ocpi.modules.versions.enums import VersionNumber + +router = APIRouter( + prefix="/sessions", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get( + "/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse +) +async def get_session( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + session_id: String(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Session. + + Retrieves a session based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - session_id (str): The ID of the session (36 characters). + + **Returns:** + The OCPIResponse containing the session data. + + **Raises:** + NotFoundOCPIError: If the session is not found. + """ + logger.info(f"Received request to get session with id - `{session_id}`.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.sessions, + RoleEnum.emsp, + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + return OCPIResponse( + data=[adapter.session_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Session with id `{session_id}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse +) +async def add_or_update_session( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + session_id: String(36), # type: ignore + session: Session, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Session. + + Adds or updates a session based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - session_id (str): The ID of the session (36 characters). + + **Request body:** + session (Session): The session object. + + **Returns:** + The OCPIResponse containing the added or updated session data. + """ + logger.info( + f"Received request to add or update session with id - `{session_id}`." + ) + logger.debug(f"Session data to update - {session.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.sessions, + RoleEnum.emsp, + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + logger.debug(f"Update session with id - `{session_id}`.") + data = await crud.update( + ModuleID.sessions, + RoleEnum.emsp, + session.dict(), + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + else: + logger.debug(f"Create session with id - `{session_id}`.") + data = await crud.create( + ModuleID.sessions, + RoleEnum.emsp, + session.dict(), + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[adapter.session_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.patch( + "/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse +) +async def partial_update_session( + request: Request, + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + session_id: String(36), # type: ignore + session: SessionPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Session. + + Partially updates a session based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - session_id (str): The ID of the session (36 characters). + + **Request body:** + session (SessionPartialUpdate): The partial session update object. + + **Returns:** + The OCPIResponse containing the partially updated session data. + + **Raises:** + NotFoundOCPIError: If the session is not found. + """ + logger.info( + f"Received request to partially update session with id - `{session_id}`." + ) + logger.debug(f"Session data to update - {session.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + old_data = await crud.get( + ModuleID.sessions, + RoleEnum.emsp, + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if old_data: + old_session = adapter.session_adapter(old_data, VersionNumber.v_2_1_1) + + new_session = deepcopy(old_session) + partially_update_attributes( + new_session, session.dict(exclude_defaults=True, exclude_unset=True) + ) + + data = await crud.update( + ModuleID.sessions, + RoleEnum.emsp, + new_session.dict(), + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + + return OCPIResponse( + data=[adapter.session_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Session with id `{session_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/sessions/v_2_1_1/enums.py b/py_ocpi/modules/sessions/v_2_1_1/enums.py new file mode 100644 index 0000000..fc493dd --- /dev/null +++ b/py_ocpi/modules/sessions/v_2_1_1/enums.py @@ -0,0 +1,24 @@ +from enum import Enum + + +class SessionStatus(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_sessions.md#41-sessionstatus-enum + """ + + # The session is accepted and active. + # Al pre-condition are met: Communication between EV and EVSE + # (for example: cable plugged in correctly), EV or Driver is authorized. + # EV is being charged, or can be charged. + # Energy is, or is not, being transferred. + active = "ACTIVE" + # The session is finished successfully. + # No more modifications will be made to this session. + completed = "COMPLETED" + # The session is declared invalid and will not be billed. + invalid = "INVALID" + # The session is pending, it has not yet started. + # Not all pre-condition are met. + # This is the initial state. + # This session might never become an active session. + pending = "PENDING" diff --git a/py_ocpi/modules/sessions/v_2_1_1/schemas.py b/py_ocpi/modules/sessions/v_2_1_1/schemas.py new file mode 100644 index 0000000..78b8c6e --- /dev/null +++ b/py_ocpi/modules/sessions/v_2_1_1/schemas.py @@ -0,0 +1,44 @@ +from typing import List, Optional +from pydantic import BaseModel + +from py_ocpi.core.data_types import Number, Price, String, DateTime +from py_ocpi.modules.cdrs.v_2_1_1.enums import AuthMethod +from py_ocpi.modules.cdrs.v_2_1_1.schemas import ChargingPeriod +from py_ocpi.modules.locations.v_2_1_1.schemas import Location +from py_ocpi.modules.sessions.v_2_1_1.enums import SessionStatus + + +class Session(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_sessions.md#31-session-object + """ + + id: String(36) # type: ignore + start_datetime: DateTime + end_datetime: Optional[DateTime] + kwh: Number + auth_id: String(36) # type: ignore + auth_method: AuthMethod + location: Location + meter_id: Optional[String(255)] # type: ignore + currency: String(3) # type: ignore + charging_periods: List[ChargingPeriod] = [] + total_cost: Optional[Price] + status: SessionStatus + last_updated: DateTime + + +class SessionPartialUpdate(BaseModel): + id: Optional[String(36)] # type: ignore + start_datetime: Optional[DateTime] + end_datetime: Optional[DateTime] + kwh: Optional[Number] + auth_id: Optional[String(36)] # type: ignore + auth_method: Optional[AuthMethod] + location: Optional[Location] + meter_id: Optional[String(255)] # type: ignore + currency: Optional[String(3)] # type: ignore + charging_periods: Optional[List[ChargingPeriod]] + total_cost: Optional[Price] + status: Optional[SessionStatus] + last_updated: Optional[DateTime] diff --git a/py_ocpi/modules/sessions/v_2_2_1/api/cpo.py b/py_ocpi/modules/sessions/v_2_2_1/api/cpo.py index 14a00a2..42a452b 100644 --- a/py_ocpi/modules/sessions/v_2_2_1/api/cpo.py +++ b/py_ocpi/modules/sessions/v_2_2_1/api/cpo.py @@ -6,30 +6,60 @@ from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters router = APIRouter( - prefix='/sessions', + prefix="/sessions", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) @router.get("/", response_model=OCPIResponse) -async def get_sessions(request: Request, - response: Response, - crud: Crud = Depends(get_crud), - adapter: Adapter = Depends(get_adapter), - filters: dict = Depends(pagination_filters)): +async def get_sessions( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get sessions. + + Retrieves a list of sessions based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return Sessions that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return Sessions that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of CDRs. + """ + logger.info("Received request to get sessions.") auth_token = get_auth_token(request) - data_list = await get_list(response, filters, ModuleID.sessions, RoleEnum.cpo, - VersionNumber.v_2_2_1, crud, auth_token=auth_token) + data_list = await get_list( + response, + filters, + ModuleID.sessions, + RoleEnum.cpo, + VersionNumber.v_2_2_1, + crud, + auth_token=auth_token, + ) sessions = [] for data in data_list: sessions.append(adapter.session_adapter(data).dict()) + logger.debug(f"Amount of sessions in response: {len(sessions)}") return OCPIResponse( data=sessions, **status.OCPI_1000_GENERIC_SUCESS_CODE, @@ -37,14 +67,37 @@ async def get_sessions(request: Request, @router.put("/{session_id}/charging_preferences", response_model=OCPIResponse) -async def set_charging_preference(request: Request, - session_id: CiString(36), - charging_preferences: ChargingPreferences, - crud: Crud = Depends(get_crud), - adapter: Adapter = Depends(get_adapter)): +async def set_charging_preference( + request: Request, + session_id: CiString(36), # type: ignore + charging_preferences: ChargingPreferences, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Set Charging Preference. + + Updates the charging preference for a specific charging session. + + **Path parameters:** + - session_id (str): The ID of the charging session (36 characters). + + **Request body:** + charging_preferences (ChargingPreferences): The charging preferences + object. + + **Returns:** + The OCPIResponse containing the updated charging preferences. + """ auth_token = get_auth_token(request) - data = await crud.update(ModuleID.sessions, RoleEnum.cpo, charging_preferences.dict(), session_id, - auth_token=auth_token, version=VersionNumber.v_2_2_1) + data = await crud.update( + ModuleID.sessions, + RoleEnum.cpo, + charging_preferences.dict(), + session_id, + auth_token=auth_token, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[adapter.charging_preference_adapter(data).dict()], **status.OCPI_1000_GENERIC_SUCESS_CODE, diff --git a/py_ocpi/modules/sessions/v_2_2_1/api/emsp.py b/py_ocpi/modules/sessions/v_2_2_1/api/emsp.py index dc30ffd..f617078 100644 --- a/py_ocpi/modules/sessions/v_2_2_1/api/emsp.py +++ b/py_ocpi/modules/sessions/v_2_2_1/api/emsp.py @@ -1,50 +1,144 @@ +import copy + from fastapi import APIRouter, Depends, Request -from py_ocpi.modules.sessions.v_2_2_1.schemas import SessionPartialUpdate, Session +from py_ocpi.modules.sessions.v_2_2_1.schemas import ( + SessionPartialUpdate, + Session, +) from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.core.utils import get_auth_token, partially_update_attributes from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError from py_ocpi.core.dependencies import get_crud, get_adapter router = APIRouter( - prefix='/sessions', + prefix="/sessions", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) + + +@router.get( + "/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse ) +async def get_session( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + session_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Session. + + Retrieves a session based on the specified parameters. + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - session_id (str): The ID of the session (36 characters). -@router.get("/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse) -async def get_session(request: Request, country_code: CiString(2), party_id: CiString(3), session_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + **Returns:** + The OCPIResponse containing the session data. + + **Raises:** + NotFoundOCPIError: If the session is not found. + """ + logger.info(f"Received request to get session with id - `{session_id}`.") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.sessions, RoleEnum.emsp, session_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - return OCPIResponse( - data=[adapter.session_adapter(data, VersionNumber.v_2_2_1).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + data = await crud.get( + ModuleID.sessions, + RoleEnum.emsp, + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) + if data: + return OCPIResponse( + data=[adapter.session_adapter(data, VersionNumber.v_2_2_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Session with id `{session_id}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse +) +async def add_or_update_session( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + session_id: CiString(36), # type: ignore + session: Session, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Session. + + Adds or updates a session based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - session_id (str): The ID of the session (36 characters). + **Request body:** + session (Session): The session object. -@router.put("/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse) -async def add_or_update_session(request: Request, country_code: CiString(2), party_id: CiString(3), - session_id: CiString(36), session: Session, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + **Returns:** + The OCPIResponse containing the added or updated session data. + """ + logger.info( + f"Received request to add or update session with id - `{session_id}`." + ) + logger.debug(f"Session data to update - {session.dict()}") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.sessions, RoleEnum.emsp, session_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) + data = await crud.get( + ModuleID.sessions, + RoleEnum.emsp, + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) if data: - data = await crud.update(ModuleID.sessions, RoleEnum.emsp, session.dict(), session_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + logger.debug(f"Update session with id - `{session_id}`.") + data = await crud.update( + ModuleID.sessions, + RoleEnum.emsp, + session.dict(), + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) else: - data = await crud.create(ModuleID.sessions, RoleEnum.emsp, session.dict(), - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + logger.debug(f"Create session with id - `{session_id}`.") + data = await crud.create( + ModuleID.sessions, + RoleEnum.emsp, + session.dict(), + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[adapter.session_adapter(data).dict()], @@ -52,24 +146,74 @@ async def add_or_update_session(request: Request, country_code: CiString(2), par ) -@router.patch("/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse) -async def partial_update_session(request: Request, country_code: CiString(2), party_id: CiString(3), - session_id: CiString(36), session: SessionPartialUpdate, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): - auth_token = get_auth_token(request) +@router.patch( + "/{country_code}/{party_id}/{session_id}", response_model=OCPIResponse +) +async def partial_update_session( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + session_id: CiString(36), # type: ignore + session: SessionPartialUpdate, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Session. - old_data = await crud.get(ModuleID.sessions, RoleEnum.emsp, session_id, auth_token=auth_token, - country_code=country_code, party_id=party_id, version=VersionNumber.v_2_2_1) - old_session = adapter.session_adapter(old_data) + Partially updates a session based on the specified parameters. - new_session = old_session - partially_update_attributes(new_session, session.dict(exclude_defaults=True, exclude_unset=True)) + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - session_id (str): The ID of the session (36 characters). - data = await crud.update(ModuleID.sessions, RoleEnum.emsp, new_session.dict(), session_id, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + **Request body:** + session (SessionPartialUpdate): The partial session update object. - return OCPIResponse( - data=[adapter.session_adapter(data).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + **Returns:** + The OCPIResponse containing the partially updated session data. + + **Raises:** + NotFoundOCPIError: If the session is not found. + """ + logger.info( + f"Received request to partially update session with id - `{session_id}`." + ) + logger.debug(f"Session data to update - {session.dict()}") + auth_token = get_auth_token(request) + + old_data = await crud.get( + ModuleID.sessions, + RoleEnum.emsp, + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, ) + if old_data: + old_session = adapter.session_adapter(old_data) + + new_session = copy.deepcopy(old_session) + partially_update_attributes( + new_session, session.dict(exclude_defaults=True, exclude_unset=True) + ) + + data = await crud.update( + ModuleID.sessions, + RoleEnum.emsp, + new_session.dict(), + session_id, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + + return OCPIResponse( + data=[adapter.session_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Session with id `{session_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/sessions/v_2_2_1/enums.py b/py_ocpi/modules/sessions/v_2_2_1/enums.py index fbd35ef..da483e5 100644 --- a/py_ocpi/modules/sessions/v_2_2_1/enums.py +++ b/py_ocpi/modules/sessions/v_2_2_1/enums.py @@ -7,15 +7,18 @@ class ChargingPreferencesResponse(str, Enum): """ # Charging Preferences accepted, EVSE will try to accomplish them, # although this is no guarantee that they will be fulfilled. - accepted = 'ACCEPTED' - # CPO requires departure_time to be able to perform Charging Preference based Smart Charging. - departure_required = 'DEPARTURE_REQUIRED' - # CPO requires energy_need to be able to perform Charging Preference based Smart Charging. - energy_need_required = 'ENERGY_NEED_REQUIRED' - # Charging Preferences contain a demand that the EVSE knows it cannot fulfill. - not_possible = 'NOT_POSSIBLE' + accepted = "ACCEPTED" + # CPO requires departure_time to be able to perform + # Charging Preference based Smart Charging. + departure_required = "DEPARTURE_REQUIRED" + # CPO requires energy_need to be able to perform + # Charging Preference based Smart Charging. + energy_need_required = "ENERGY_NEED_REQUIRED" + # Charging Preferences contain a demand that + # the EVSE knows it cannot fulfill. + not_possible = "NOT_POSSIBLE" # profile_type contains a value that is not supported by the EVSE. - profile_type_not_supported = 'PROFILE_TYPE_NOT_SUPPORTED' + profile_type_not_supported = "PROFILE_TYPE_NOT_SUPPORTED" class ProfileType(str, Enum): @@ -23,31 +26,38 @@ class ProfileType(str, Enum): https://github.com/ocpi/ocpi/blob/2.2.1/mod_sessions.asciidoc#142-profiletype-enum """ # Driver wants to use the cheapest charging profile possible. - cheap = 'CHEAP' - # Driver wants his EV charged as quickly as possible and is willing to pay a premium for this, if needed. - fast = 'FAST' - # Driver wants his EV charged with as much regenerative (green) energy as possible. - green = 'GREEN' + cheap = "CHEAP" + # Driver wants his EV charged as quickly as possible + # and is willing to pay a premium for this, if needed. + fast = "FAST" + # Driver wants his EV charged with as much regenerative + # (green) energy as possible. + green = "GREEN" # Driver does not have special preferences. - regular = 'REGULAR' + regular = "REGULAR" class SessionStatus(str, Enum): """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_sessions.asciidoc#143-sessionstatus-enum """ - # The session has been accepted and is active. All pre-conditions were met: Communication between EV and EVSE - # (for example: cable plugged in correctly), EV or driver is authorized. EV is being charged, or can be charged. + # The session has been accepted and is active. + # All pre-conditions were met: Communication between EV and EVSE + # (for example: cable plugged in correctly), + # EV or driver is authorized. EV is being charged, or can be charged. # Energy is, or is not, being transfered. - active = 'ACTIVE' + active = "ACTIVE" # The session has been finished successfully. # No more modifications will be made to the Session object using this state. - completed = 'COMPLETED' - # The Session object using this state is declared invalid and will not be billed. - invalid = 'INVALID' - # The session is pending, it has not yet started. Not all pre-conditions are met. - # This is the initial state. The session might never become an active session. - pending = 'PENDING' + completed = "COMPLETED" + # The Session object using this state is declared invalid + # and will not be billed. + invalid = "INVALID" + # The session is pending, it has not yet started. + # Not all pre-conditions are met. + # This is the initial state. + # The session might never become an active session. + pending = "PENDING" # The session is started due to a reservation, charging has not yet started. # The session might never become an active session. - reservation = 'RESERVATION' + reservation = "RESERVATION" diff --git a/py_ocpi/modules/tariffs/enums.py b/py_ocpi/modules/tariffs/enums.py new file mode 100644 index 0000000..6226042 --- /dev/null +++ b/py_ocpi/modules/tariffs/enums.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class DayOfWeek(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/mod_tariffs.asciidoc#141-dayofweek-enum + """ + monday = "MONDAY" + tuesday = "TUESDAY" + wednesday = "WEDNESDAY" + thursday = "THURSDAY" + friday = "FRIDAY" + saturday = "SATURDAY" + sunday = "SUNDAY" 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..fefbbac --- /dev/null +++ b/py_ocpi/modules/tariffs/v_2_1_1/api/cpo.py @@ -0,0 +1,66 @@ +from fastapi import APIRouter, Depends, Response, Request + +from py_ocpi.core.utils import get_list, get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +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", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@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), +): + """ + Get Tariffs. + + Retrieves a list of tariffs based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return tariffs that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return tariffs that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of tariffs. + """ + logger.info("Received request to get tariffs") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + 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() + ) + logger.debug(f"Amount of tariffs in response: {len(tariffs)}") + 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..b0329d5 --- /dev/null +++ b/py_ocpi/modules/tariffs/v_2_1_1/api/emsp.py @@ -0,0 +1,277 @@ +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.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +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.exceptions import NotFoundOCPIError +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.utils import ( + get_auth_token, + 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", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@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), +): + """ + Get Tariff. + + Retrieves a tariff based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - tariff_id (str): The ID of the tariff (36 characters). + + **Returns:** + The OCPIResponse containing the tariff data. + + **Raises:** + NotFoundOCPIError: If the tariff is not found. + """ + logger.info(f"Received request to get tariff with id - `{tariff_id}`.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + 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: + return OCPIResponse( + data=[adapter.tariff_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Tariff with id `{tariff_id}` was not found.") + raise NotFoundOCPIError + + +@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), +): + """ + Add or Update Tariff. + + Adds or updates a tariff based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - tariff_id (str): The ID of the tariff (36 characters). + + **Request body:** + tariff (Tariff): The tariff object. + + **Returns:** + The OCPIResponse containing the tariff data. + """ + logger.info( + f"Received request to add or update tariff with id - `{tariff_id}`." + ) + logger.debug(f"Tariff data to update - {tariff.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + 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: + logger.debug(f"Update tariff with id - `{tariff_id}`.") + 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: + logger.debug(f"Create tariff with id - `{tariff_id}`.") + 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), +): + """ + Partial Update Tariff. + + Partially updates a tariff based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - tariff_id (str): The ID of the tariff (36 characters). + + **Request body:** + tariff (TariffPartialUpdate): The partial tariff update object. + + **Returns:** + The OCPIResponse containing the partially updated tariff data. + + **Raises:** + NotFoundOCPIError: If the tariff is not found. + """ + logger.info( + f"Received request to partially update tariff with id - `{tariff_id}`." + ) + logger.debug(f"Tariff data to update - {tariff.dict()}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + 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, + ) + if old_data: + 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, + ) + logger.debug(f"Tariff with id `{tariff_id}` was not found.") + raise NotFoundOCPIError + + +@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), +): + """ + Delete Tariff. + + Deletes a tariff based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - tariff_id (str): The ID of the tariff to delete (36 characters). + + **Returns:** + The OCPIResponse indicating the success of the operation. + + **Raises:** + NotFoundOCPIError: If the tariff is not found. + """ + logger.info(f"Received request to delete tariff with id - `{tariff_id}`.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + tariff = 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 tariff: + 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, + ) + logger.debug(f"Tariff with id `{tariff_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/tariffs/v_2_1_1/enums.py b/py_ocpi/modules/tariffs/v_2_1_1/enums.py new file mode 100644 index 0000000..069491a --- /dev/null +++ b/py_ocpi/modules/tariffs/v_2_1_1/enums.py @@ -0,0 +1,16 @@ +from py_ocpi.modules.tariffs.enums import * # noqa + + +class TariffDimensionType(str, Enum): # noqa + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#44-tariffdimensiontype-enum + """ + + # defined in kWh, step_size multiplier: 1 Wh + energy = "ENERGY" + # flat fee, no unit + flat = "FLAT" + # time not charging: defined in hours, step_size multiplier: 1 second + parking_time = "PARKING_TIME" + # time charging: defined in hours, step_size multiplier: 1 second + time = "TIME" diff --git a/py_ocpi/modules/tariffs/v_2_1_1/schemas.py b/py_ocpi/modules/tariffs/v_2_1_1/schemas.py new file mode 100644 index 0000000..dbd32c5 --- /dev/null +++ b/py_ocpi/modules/tariffs/v_2_1_1/schemas.py @@ -0,0 +1,71 @@ +from typing import List, Optional + +from pydantic import BaseModel + +from py_ocpi.core.data_types import URL, DisplayText, Number, String, DateTime +from py_ocpi.modules.locations.v_2_1_1.schemas import EnergyMix +from py_ocpi.modules.tariffs.v_2_1_1.enums import ( + DayOfWeek, + TariffDimensionType, +) + + +class PriceComponent(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#42-pricecomponent-class + """ + + type: TariffDimensionType + price: Number + step_size: int + + +class TariffRestrictions(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#45-tariffrestrictions-class + """ + + start_time: Optional[String(5)] # type: ignore + end_time: Optional[String(5)] # type: ignore + start_date: Optional[String(10)] # type: ignore + end_date: Optional[String(10)] # type: ignore + min_kwh: Optional[Number] + max_kwh: Optional[Number] + min_power: Optional[Number] + max_power: Optional[Number] + min_duration: Optional[int] + max_duration: Optional[int] + day_of_week: List[DayOfWeek] = [] + + +class TariffElement(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#43-tariffelement-class + """ + + price_components: List[PriceComponent] + restrictions: Optional[TariffRestrictions] + + +class Tariff(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#31-tariff-object + """ + + id: String(36) # type: ignore + currency: String(3) # type: ignore + tariff_alt_text: List[DisplayText] = [] + tariff_alt_url: Optional[URL] + 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/modules/tariffs/v_2_2_1/api/cpo.py b/py_ocpi/modules/tariffs/v_2_2_1/api/cpo.py index 7f5dcc7..23dc6be 100644 --- a/py_ocpi/modules/tariffs/v_2_2_1/api/cpo.py +++ b/py_ocpi/modules/tariffs/v_2_2_1/api/cpo.py @@ -4,30 +4,60 @@ from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger 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', + prefix="/tariffs", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) @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)): +async def get_tariffs( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get Tariffs. + + Retrieves a list of tariffs based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return tariffs that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return tariffs that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of tariffs. + """ + logger.info("Received request to get tariffs") auth_token = get_auth_token(request) - data_list = await get_list(response, filters, ModuleID.tariffs, RoleEnum.cpo, - VersionNumber.v_2_2_1, crud, auth_token=auth_token) + data_list = await get_list( + response, + filters, + ModuleID.tariffs, + RoleEnum.cpo, + VersionNumber.v_2_2_1, + crud, + auth_token=auth_token, + ) tariffs = [] for data in data_list: tariffs.append(adapter.tariff_adapter(data).dict()) + logger.debug(f"Amount of tariffs in response: {len(tariffs)}") return OCPIResponse( data=tariffs, **status.OCPI_1000_GENERIC_SUCESS_CODE, diff --git a/py_ocpi/modules/tariffs/v_2_2_1/api/emsp.py b/py_ocpi/modules/tariffs/v_2_2_1/api/emsp.py index f10b0ad..36e2bb7 100644 --- a/py_ocpi/modules/tariffs/v_2_2_1/api/emsp.py +++ b/py_ocpi/modules/tariffs/v_2_2_1/api/emsp.py @@ -6,45 +6,134 @@ from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError from py_ocpi.core.dependencies import get_crud, get_adapter router = APIRouter( - prefix='/tariffs', + prefix="/tariffs", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) -@router.get("/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse) -async def get_tariff(request: Request, country_code: CiString(2), party_id: CiString(3), tariff_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.get( + "/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse +) +async def get_tariff( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + tariff_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Tariff. + + Retrieves a tariff based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - tariff_id (str): The ID of the tariff (36 characters). + + **Returns:** + The OCPIResponse containing the tariff data. + + **Raises:** + NotFoundOCPIError: If the tariff is not found. + """ + logger.info(f"Received request to get tariff with id - `{tariff_id}`.") auth_token = get_auth_token(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_2_1) - return OCPIResponse( - data=[adapter.tariff_adapter(data, VersionNumber.v_2_2_1).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + 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_2_1, ) + if data: + return OCPIResponse( + data=[adapter.tariff_adapter(data, VersionNumber.v_2_2_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Tariff with id `{tariff_id}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse +) +async def add_or_update_tariff( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + tariff_id: CiString(36), # type: ignore + tariff: Tariff, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Tariff. + + Adds or updates a tariff based on the specified parameters. + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - tariff_id (str): The ID of the tariff (36 characters). -@router.put("/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse) -async def add_or_update_tariff(request: Request, country_code: CiString(2), party_id: CiString(3), - tariff_id: CiString(36), tariff: Tariff, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + **Request body:** + tariff (Tariff): The tariff object. + + **Returns:** + The OCPIResponse containing the tariff data. + """ + logger.info( + f"Received request to add or update tariff with id - `{tariff_id}`." + ) + logger.debug(f"Tariff data to update - {tariff.dict()}") auth_token = get_auth_token(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_2_1) + 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_2_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_2_1) + logger.debug(f"Update tariff with id - `{tariff_id}`.") + 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_2_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_2_1) + logger.debug(f"Create tariff with id - `{tariff_id}`.") + 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_2_1, + ) return OCPIResponse( data=[adapter.tariff_adapter(data).dict()], @@ -52,16 +141,58 @@ async def add_or_update_tariff(request: Request, country_code: CiString(2), part ) -@router.delete("/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse) -async def delete_tariff(request: Request, country_code: CiString(2), party_id: CiString(3), tariff_id: CiString(36), - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): - auth_token = get_auth_token(request) +@router.delete( + "/{country_code}/{party_id}/{tariff_id}", response_model=OCPIResponse +) +async def delete_tariff( + request: Request, + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + tariff_id: CiString(36), # type: ignore + crud: Crud = Depends(get_crud), +): + """ + Delete Tariff. - 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_2_1) + Deletes a tariff based on the specified parameters. - return OCPIResponse( - data=[], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - tariff_id (str): The ID of the tariff to delete (36 characters). + + **Returns:** + The OCPIResponse indicating the success of the operation. + + **Raises:** + NotFoundOCPIError: If the tariff is not found. + """ + logger.info(f"Received request to delete tariff with id - `{tariff_id}`.") + auth_token = get_auth_token(request) + + tariff = 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_2_1, ) + if tariff: + 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_2_1, + ) + + return OCPIResponse( + data=[], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Tariff with id `{tariff_id}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/tariffs/v_2_2_1/enums.py b/py_ocpi/modules/tariffs/v_2_2_1/enums.py index b7bb3cc..6f250c8 100644 --- a/py_ocpi/modules/tariffs/v_2_2_1/enums.py +++ b/py_ocpi/modules/tariffs/v_2_2_1/enums.py @@ -1,58 +1,52 @@ -from enum import Enum +from py_ocpi.modules.tariffs.enums import * # noqa -class DayOfWeek(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/mod_tariffs.asciidoc#141-dayofweek-enum - """ - monday = 'MONDAY' - tuesday = 'TUESDAY' - wednesday = 'WEDNESDAY' - thursday = 'THURSDAY' - friday = 'FRIDAY' - saturday = 'SATURDAY' - sunday = 'SUNDAY' - - -class ReservationRestrictionType(str, Enum): +class ReservationRestrictionType(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_tariffs.asciidoc#143-reservationrestrictiontype-enum """ # Used in TariffElements to describe costs for a reservation. - reservation = 'RESERVATION' + reservation = "RESERVATION" # Used in TariffElements to describe costs for a reservation that expires - # (i.e. driver does not start a charging session before expiry_date of the reservation). - reservation_expires = 'RESERVATION_EXPIRES' + # (i.e. driver does not start a charging session before + # expiry_date of the reservation). + reservation_expires = "RESERVATION_EXPIRES" -class TariffDimensionType(str, Enum): +class TariffDimensionType(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_tariffs.asciidoc#145-tariffdimensiontype-enum """ # Defined in kWh, step_size multiplier: 1 Wh - energy = 'ENERGY' + energy = "ENERGY" # Flat fee without unit for step_size - flat = 'FLAT' + flat = "FLAT" # Time not charging: defined in hours, step_size multiplier: 1 second - parking_time = 'PARKING_TIME' + parking_time = "PARKING_TIME" # Time charging: defined in hours, step_size multiplier: 1 second - # Can also be used in combination with a RESERVATION restriction to describe the price of the reservation time. - time = 'TIME' + # Can also be used in combination with a RESERVATION + # restriction to describe the price of the reservation time. + time = "TIME" -class TariffType(str, Enum): +class TariffType(str, Enum): # noqa """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_tariffs.asciidoc#147-tarifftype-enum """ - # Used to describe that a Tariff is valid when ad-hoc payment is used at the Charge Point + # Used to describe that a Tariff is valid when ad-hoc payment is used + # at the Charge Point # (for example: Debit or Credit card payment terminal). - ad_hoc_payment = 'AD_HOC_PAYMENT' - # Used to describe that a Tariff is valid when Charging Preference: CHEAP is set for the session. - profile_cheap = 'PROFILE_CHEAP' - # Used to describe that a Tariff is valid when Charging Preference: FAST is set for the session. - profile_fast = 'PROFILE_FAST' - # Used to describe that a Tariff is valid when Charging Preference: GREEN is set for the session. - profile_green = 'PROFILE_GREEN' - # Used to describe that a Tariff is valid when using an RFID, without any Charging Preference, + ad_hoc_payment = "AD_HOC_PAYMENT" + # Used to describe that a Tariff is valid when Charging Preference: + # CHEAP is set for the session. + profile_cheap = "PROFILE_CHEAP" + # Used to describe that a Tariff is valid when Charging Preference: + # FAST is set for the session. + profile_fast = "PROFILE_FAST" + # Used to describe that a Tariff is valid when Charging Preference: + # GREEN is set for the session. + profile_green = "PROFILE_GREEN" + # Used to describe that a Tariff is valid when using an RFID, + # without any Charging Preference, # or when Charging Preference: REGULAR is set for the session. - regular = 'REGULAR' + regular = "REGULAR" diff --git a/py_ocpi/modules/tariffs/v_2_2_1/schemas.py b/py_ocpi/modules/tariffs/v_2_2_1/schemas.py index a9319b9..aedb00d 100644 --- a/py_ocpi/modules/tariffs/v_2_2_1/schemas.py +++ b/py_ocpi/modules/tariffs/v_2_2_1/schemas.py @@ -3,8 +3,21 @@ from pydantic import BaseModel from py_ocpi.modules.locations.v_2_2_1.schemas import EnergyMix -from py_ocpi.modules.tariffs.v_2_2_1.enums import DayOfWeek, ReservationRestrictionType, TariffDimensionType, TariffType -from py_ocpi.core.data_types import URL, CiString, DisplayText, Number, Price, String, DateTime +from py_ocpi.modules.tariffs.v_2_2_1.enums import ( + DayOfWeek, + ReservationRestrictionType, + TariffDimensionType, + TariffType, +) +from py_ocpi.core.data_types import ( + URL, + CiString, + DisplayText, + Number, + Price, + String, + DateTime, +) class PriceComponent(BaseModel): diff --git a/py_ocpi/modules/tokens/v_2_1_1/api/__init__.py b/py_ocpi/modules/tokens/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..92b6e31 --- /dev/null +++ b/py_ocpi/modules/tokens/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/tokens/v_2_1_1/api/cpo.py b/py_ocpi/modules/tokens/v_2_1_1/api/cpo.py new file mode 100644 index 0000000..3eec9ff --- /dev/null +++ b/py_ocpi/modules/tokens/v_2_1_1/api/cpo.py @@ -0,0 +1,218 @@ +from copy import deepcopy + +from fastapi import APIRouter, Request, Depends + +from py_ocpi.core import status +from py_ocpi.core.data_types import String +from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.utils import ( + get_auth_token, + partially_update_attributes, +) +from py_ocpi.core.dependencies import get_crud, get_adapter +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.modules.tokens.v_2_1_1.schemas import Token, TokenPartialUpdate + +router = APIRouter( + prefix="/tokens", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get( + "/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse +) +async def get_token( + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + token_uid: String(36), # type: ignore + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Token. + + Retrieves a token based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - token_uid (str): The ID of the token (36 characters). + + **Returns:** + The OCPIResponse containing the token data. + + **Raises:** + NotFoundOCPIError: If the token is not found. + """ + logger.info(f"Received request to get token with id - `{token_uid}`.") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.tokens, + RoleEnum.cpo, + token_uid, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + return OCPIResponse( + data=[adapter.token_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Token with id `{token_uid}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse +) +async def add_or_update_token( + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + token_uid: String(36), # type: ignore + token: Token, + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Token. + + Adds or updates a token based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - token_uid (str): The ID of the token (36 characters). + + **Request body:** + token (Token): The token object. + + **Returns:** + The OCPIResponse containing the token data. + """ + logger.info( + f"Received request to add or update token with id - `{token_uid}`." + ) + logger.debug(f"Token data to update - {token}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data = await crud.get( + ModuleID.tokens, + RoleEnum.cpo, + token_uid, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if data: + logger.debug(f"Update token with id - `{token_uid}`.") + data = await crud.update( + ModuleID.tokens, + RoleEnum.cpo, + token.dict(), + token_uid, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + else: + logger.debug(f"Create token with id - `{token_uid}`.") + data = await crud.create( + ModuleID.tokens, + RoleEnum.cpo, + token.dict(), + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + return OCPIResponse( + data=[adapter.token_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.patch( + "/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse +) +async def partial_update_token( + country_code: String(2), # type: ignore + party_id: String(3), # type: ignore + token_uid: String(36), # type: ignore + token: TokenPartialUpdate, + request: Request, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Token. + + Partially updates a token based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - token_uid (str): The ID of the token (36 characters). + + **Request body:** + token (TokenPartialUpdate): The partial token update object. + + **Returns:** + The OCPIResponse containing the partially updated token data. + + **Raises:** + NotFoundOCPIError: If the token is not found. + """ + logger.info( + f"Received request to partially update token with id - `{token_uid}`." + ) + logger.debug(f"Token data to update - {token}") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + old_data = await crud.get( + ModuleID.tokens, + RoleEnum.cpo, + token_uid, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + if not old_data: + logger.debug(f"Token with id `{token_uid}` was not found.") + + raise NotFoundOCPIError + old_token = adapter.token_adapter(old_data, VersionNumber.v_2_1_1) + + new_token = deepcopy(old_token) + partially_update_attributes( + new_token, token.dict(exclude_defaults=True, exclude_unset=True) + ) + + data = await crud.update( + ModuleID.tokens, + RoleEnum.cpo, + new_token.dict(), + token_uid, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_1_1, + ) + return OCPIResponse( + data=[adapter.token_adapter(data, VersionNumber.v_2_1_1).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/tokens/v_2_1_1/api/emsp.py b/py_ocpi/modules/tokens/v_2_1_1/api/emsp.py new file mode 100644 index 0000000..3bda0eb --- /dev/null +++ b/py_ocpi/modules/tokens/v_2_1_1/api/emsp.py @@ -0,0 +1,152 @@ +from fastapi import APIRouter, Depends, Response, Request + +from py_ocpi.modules.tokens.v_2_1_1.enums import TokenType +from py_ocpi.modules.tokens.v_2_1_1.schemas import LocationReference +from py_ocpi.modules.versions.enums import VersionNumber +from py_ocpi.core.utils import get_list, get_auth_token +from py_ocpi.core import status +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier +from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.core.data_types import String +from py_ocpi.core.enums import ModuleID, RoleEnum, Action +from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters + +router = APIRouter( + prefix="/tokens", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))], +) + + +@router.get("/", response_model=OCPIResponse) +async def get_tokens( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get Tokens. + + Retrieves a list of tokens based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return tokens that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return tokens that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of tokens. + """ + logger.info("Received request to get tokens") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + data_list = await get_list( + response, + filters, + ModuleID.tokens, + RoleEnum.emsp, + VersionNumber.v_2_1_1, + crud, + auth_token=auth_token, + ) + + tokens = [] + for data in data_list: + tokens.append(adapter.token_adapter(data, VersionNumber.v_2_1_1).dict()) + logger.debug(f"Amount of tokens in response: {len(tokens)}") + return OCPIResponse( + data=tokens, + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + +@router.post("/{token_uid}/authorize", response_model=OCPIResponse) +async def authorize_token( + request: Request, + token_uid: String(36), # type: ignore + token_type: TokenType = TokenType.rfid, + location_reference: LocationReference = None, # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Authorize Token. + + Authorizes a token based on the specified parameters. + + **Path parameters:** + - token_uid (str): The ID of the token to authorize (36 characters). + + **Query parameters:** + - token_type (TokenType): The type of the token (default=TokenType.rfid). + + **Request body:** + - location_reference (LocationReference): The location reference + for authorization (default=None). + + **Returns:** + The OCPIResponse containing the authorization result. + + **Raises:** + NotFoundOCPIError: If the token is not found. + """ + logger.info(f"Received request to authorize token with id `{token_uid}`") + logger.debug(f"Token type - `{token_type}`") + logger.debug(f"Location reference - `{location_reference}`") + auth_token = get_auth_token(request, VersionNumber.v_2_1_1) + + # check if token exists + token = await crud.get( + ModuleID.tokens, + RoleEnum.emsp, + token_uid, + auth_token=auth_token, + token_type=token_type, + version=VersionNumber.v_2_1_1, + ) + if token: + location_reference = ( + location_reference.dict() + if location_reference + else None # type: ignore + ) + data = { + "token_uid": token_uid, + "token_type": token_type, + "location_reference": location_reference, + } + authroization_result = await crud.do( + ModuleID.tokens, + RoleEnum.emsp, + Action.authorize_token, + data=data, + auth_token=auth_token, + ) + + # when the token information is not enough + if not authroization_result: + logger.debug("Authorization result is null.") + return OCPIResponse( + data=[], + **status.OCPI_2002_NOT_ENOUGH_INFORMATION, + ) + + return OCPIResponse( + data=[ + adapter.authorization_adapter( + authroization_result, VersionNumber.v_2_1_1 + ).dict() + ], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + + logger.debug(f"Token with id `{token_uid}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/tokens/v_2_1_1/enums.py b/py_ocpi/modules/tokens/v_2_1_1/enums.py new file mode 100644 index 0000000..f61ca64 --- /dev/null +++ b/py_ocpi/modules/tokens/v_2_1_1/enums.py @@ -0,0 +1,49 @@ +from enum import Enum + + +class Allowed(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tokens.md#41-allowed-enum + """ + + # This Token is allowed to charge at this location. + allowed = "ALLOWED" + # This Token is blocked. + blocked = "BLOCKED" + # This Token has expired. + expired = "EXPIRED" + # This Token belongs to an account that has not enough credits to charge + # at the given location. + no_credit = "NO_CREDIT" + # Token is valid, but is not allowed to charge at the given location. + not_allowed = "NOT_ALLOWED" + + +class TokenType(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tokens.md#43-tokentype-enum + """ + + # Other type of token + other = "OTHER" + # RFID Token + rfid = "RFID" + + +class WhitelistType(str, Enum): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tokens.md#44-whitelisttype-enum + """ + + # Token always has to be whitelisted, realtime authorization + # is not possible/allowed. + always = "ALWAYS" + # It is allowed to whitelist the token, realtime authorization + # is also allowed. + allowed = "ALLOWED" + # Whitelisting is only allowed when CPO cannot reach the eMSP + # (communication between CPO and eMSP is offline) + allowed_offline = "ALLOWED_OFFLINE" + # Whitelisting is forbidden, only realtime authorization is allowed. + # Token should always be authorized by the eMSP. + never = "NEVER" diff --git a/py_ocpi/modules/tokens/v_2_1_1/schemas.py b/py_ocpi/modules/tokens/v_2_1_1/schemas.py new file mode 100644 index 0000000..a864303 --- /dev/null +++ b/py_ocpi/modules/tokens/v_2_1_1/schemas.py @@ -0,0 +1,57 @@ +from typing import Optional, List +from pydantic import BaseModel + +from py_ocpi.core.data_types import String, DisplayText, DateTime +from py_ocpi.modules.tokens.v_2_1_1.enums import ( + Allowed, + TokenType, + WhitelistType, +) + + +class LocationReference(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tokens.md#42-locationreferences-class + """ + + location_id: String(39) # type: ignore + evse_uids: List[String(39)] = [] # type: ignore + connector_ids: List[String(36)] = [] # type: ignore + + +class Token(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tokens.md#32-token-object + """ + + uid: String(36) # type: ignore + type: TokenType + auth_id: String(36) # type: ignore + visual_number: Optional[String(64)] # type: ignore + issuer: String(64) # type: ignore + valid: bool + whitelist: WhitelistType + language: Optional[String(2)] # type: ignore + last_updated: DateTime + + +class TokenPartialUpdate(BaseModel): + uid: Optional[String(36)] # type: ignore + type: Optional[TokenType] + auth_id: Optional[String(36)] # type: ignore + visual_number: Optional[String(64)] # type: ignore + issuer: Optional[String(64)] # type: ignore + valid: Optional[bool] + whitelist: Optional[WhitelistType] + language: Optional[String(2)] # type: ignore + last_updated: Optional[DateTime] + + +class AuthorizationInfo(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tokens.md#31-authorizationinfo-object + """ + + allowed: Allowed + location: Optional[LocationReference] + info: Optional[DisplayText] diff --git a/py_ocpi/modules/tokens/v_2_2_1/api/cpo.py b/py_ocpi/modules/tokens/v_2_2_1/api/cpo.py index 937052f..f8132ac 100644 --- a/py_ocpi/modules/tokens/v_2_2_1/api/cpo.py +++ b/py_ocpi/modules/tokens/v_2_2_1/api/cpo.py @@ -1,11 +1,16 @@ +import copy + from fastapi import APIRouter, Request, Depends from py_ocpi.core import status from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum +from py_ocpi.core.exceptions import NotFoundOCPIError from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.utils import get_auth_token, partially_update_attributes from py_ocpi.core.dependencies import get_crud, get_adapter from py_ocpi.modules.versions.enums import VersionNumber @@ -13,66 +18,216 @@ from py_ocpi.modules.tokens.v_2_2_1.schemas import Token, TokenPartialUpdate router = APIRouter( - prefix='/tokens', + prefix="/tokens", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], +) + + +@router.get( + "/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse ) +async def get_token( + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + token_uid: CiString(36), # type: ignore + request: Request, + token_type: TokenType = TokenType.rfid, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Get Token. + Retrieves information about a token based on the specified parameters. -@router.get("/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse) -async def get_token(country_code: CiString(2), party_id: CiString(3), token_uid: CiString(36), - request: Request, token_type: TokenType = TokenType.rfid, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - token_uid (str): The ID of the token (36 characters). + + **Query parameters:** + - token_type (TokenType): The type of the token (default=TokenType.rfid). + + **Returns:** + The OCPIResponse containing the token information. + + **Raises:** + NotFoundOCPIError: If the token is not found. + """ + logger.info(f"Received request to get token with id - `{token_uid}`.") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.tokens, RoleEnum.cpo, token_uid, - auth_token=auth_token, country_code=country_code, - party_id=party_id, token_type=token_type, - version=VersionNumber.v_2_2_1) - return OCPIResponse( - data=[adapter.token_adapter(data).dict()], - **status.OCPI_1000_GENERIC_SUCESS_CODE, + data = await crud.get( + ModuleID.tokens, + RoleEnum.cpo, + token_uid, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + token_type=token_type, + version=VersionNumber.v_2_2_1, ) + if data: + return OCPIResponse( + data=[adapter.token_adapter(data).dict()], + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) + logger.debug(f"Token with id `{token_uid}` was not found.") + raise NotFoundOCPIError + + +@router.put( + "/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse +) +async def add_or_update_token( + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + token_uid: CiString(36), # type: ignore + token: Token, + request: Request, + token_type: TokenType = TokenType.rfid, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Add or Update Token. + + Adds or updates a token based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - token_uid (str): The ID of the token (36 characters). + **Query parameters:** + - token_type (TokenType): The type of the token (default=TokenType.rfid). -@router.put("/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse) -async def add_or_update_token(country_code: CiString(2), party_id: CiString(3), token_uid: CiString(36), token: Token, - request: Request, token_type: TokenType = TokenType.rfid, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): + **Request body:** + token (Token): The token object. + + **Returns:** + The OCPIResponse containing the token data. + """ + logger.info( + f"Received request to add or update token with id - `{token_uid}`." + ) + logger.debug(f"Token data to update - {token.dict()}") auth_token = get_auth_token(request) - data = await crud.get(ModuleID.tokens, RoleEnum.cpo, token_uid, auth_token=auth_token, - token_type=token_type, country_code=country_code, party_id=party_id, - version=VersionNumber.v_2_2_1) + data = await crud.get( + ModuleID.tokens, + RoleEnum.cpo, + token_uid, + auth_token=auth_token, + token_type=token_type, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) if data: - data = await crud.update(ModuleID.tokens, RoleEnum.cpo, token.dict(), token_uid, token_type=token_type, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + logger.debug(f"Update token with id - `{token_uid}`.") + data = await crud.update( + ModuleID.tokens, + RoleEnum.cpo, + token.dict(), + token_uid, + token_type=token_type, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) else: - data = await crud.create(ModuleID.tokens, RoleEnum.cpo, token.dict(), token_type=token_type, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + logger.debug(f"Create token with id - `{token_uid}`.") + data = await crud.create( + ModuleID.tokens, + RoleEnum.cpo, + token.dict(), + token_type=token_type, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[adapter.token_adapter(data).dict()], **status.OCPI_1000_GENERIC_SUCESS_CODE, ) -@router.patch("/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse) -async def partial_update_token(country_code: CiString(2), party_id: CiString(3), token_uid: CiString(36), - token: TokenPartialUpdate, request: Request, token_type: TokenType = TokenType.rfid, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +@router.patch( + "/{country_code}/{party_id}/{token_uid}", response_model=OCPIResponse +) +async def partial_update_token( + country_code: CiString(2), # type: ignore + party_id: CiString(3), # type: ignore + token_uid: CiString(36), # type: ignore + token: TokenPartialUpdate, + request: Request, + token_type: TokenType = TokenType.rfid, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Partial Update Token. + + Partially updates a token based on the specified parameters. + + **Path parameters:** + - country_code (str): The two-letter country code. + - party_id (str): The three-letter party ID. + - token_uid (str): The ID of the token (36 characters). + + **Query parameters:** + - token_type (TokenType): The type of the token (default=TokenType.rfid). + + **Request body:** + token (TokenPartialUpdate): The partial token update object. + + **Returns:** + The OCPIResponse containing the partially updated token data. + + **Raises:** + NotFoundOCPIError: If the token is not found. + """ + logger.info( + f"Received request to partially update token with id - `{token_uid}`." + ) + logger.debug(f"Token data to update - {token.dict()}") auth_token = get_auth_token(request) - old_data = await crud.get(ModuleID.tokens, RoleEnum.cpo, token_uid, token_type=token_type, - auth_token=auth_token, country_code=country_code, party_id=party_id, - version=VersionNumber.v_2_2_1) + old_data = await crud.get( + ModuleID.tokens, + RoleEnum.cpo, + token_uid, + token_type=token_type, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) + if not old_data: + logger.debug(f"Token with id `{token_uid}` was not found.") + + raise NotFoundOCPIError old_token = adapter.token_adapter(old_data) - new_token = old_token - partially_update_attributes(new_token, token.dict(exclude_defaults=True, exclude_unset=True)) + new_token = copy.deepcopy(old_token) + partially_update_attributes( + new_token, token.dict(exclude_defaults=True, exclude_unset=True) + ) - data = await crud.update(ModuleID.tokens, RoleEnum.cpo, new_token.dict(), token_uid, token_type=token_type, - auth_token=auth_token, country_code=country_code, - party_id=party_id, version=VersionNumber.v_2_2_1) + data = await crud.update( + ModuleID.tokens, + RoleEnum.cpo, + new_token.dict(), + token_uid, + token_type=token_type, + auth_token=auth_token, + country_code=country_code, + party_id=party_id, + version=VersionNumber.v_2_2_1, + ) return OCPIResponse( data=[adapter.token_adapter(data).dict()], **status.OCPI_1000_GENERIC_SUCESS_CODE, diff --git a/py_ocpi/modules/tokens/v_2_2_1/api/emsp.py b/py_ocpi/modules/tokens/v_2_2_1/api/emsp.py index 0fdb89e..b5b374a 100644 --- a/py_ocpi/modules/tokens/v_2_2_1/api/emsp.py +++ b/py_ocpi/modules/tokens/v_2_2_1/api/emsp.py @@ -1,38 +1,67 @@ -from fastapi import APIRouter, Depends, Response, Request, status as http_status +from fastapi import APIRouter, Depends, Response, Request from py_ocpi.modules.tokens.v_2_2_1.enums import TokenType -from py_ocpi.modules.tokens.v_2_2_1.schemas import LocationReference, AuthorizationInfo +from py_ocpi.modules.tokens.v_2_2_1.schemas import LocationReference from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.core.utils import get_list, get_auth_token from py_ocpi.core import status from py_ocpi.core.schemas import OCPIResponse from py_ocpi.core.adapter import Adapter +from py_ocpi.core.authentication.verifier import AuthorizationVerifier from py_ocpi.core.crud import Crud +from py_ocpi.core.config import logger from py_ocpi.core.exceptions import NotFoundOCPIError from py_ocpi.core.data_types import CiString from py_ocpi.core.enums import ModuleID, RoleEnum, Action from py_ocpi.core.dependencies import get_crud, get_adapter, pagination_filters router = APIRouter( - prefix='/tokens', + prefix="/tokens", + dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_2_1))], ) @router.get("/", response_model=OCPIResponse) -async def get_tokens(request: Request, - response: Response, - crud: Crud = Depends(get_crud), - adapter: Adapter = Depends(get_adapter), - filters: dict = Depends(pagination_filters)): +async def get_tokens( + request: Request, + response: Response, + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), + filters: dict = Depends(pagination_filters), +): + """ + Get Tokens. + + Retrieves a list of tokens based on the specified filters. + + **Query parameters:** + - limit (int): Maximum number of objects to GET (default=50). + - offset (int): The offset of the first object returned (default=0). + - date_from (datetime): Only return tokens that have last_updated + after this Date/Time (default=None). + - date_to (datetime): Only return tokens that have last_updated + before this Date/Time (default=None). + + **Returns:** + The OCPIResponse containing the list of tokens. + """ + logger.info("Received request to get tokens") auth_token = get_auth_token(request) - data_list = await get_list(response, filters, ModuleID.tokens, RoleEnum.emsp, - VersionNumber.v_2_2_1, crud, auth_token=auth_token) + data_list = await get_list( + response, + filters, + ModuleID.tokens, + RoleEnum.emsp, + VersionNumber.v_2_2_1, + crud, + auth_token=auth_token, + ) tokens = [] for data in data_list: tokens.append(adapter.token_adapter(data).dict()) - + logger.debug(f"Amount of tokens in response: {len(tokens)}") return OCPIResponse( data=tokens, **status.OCPI_1000_GENERIC_SUCESS_CODE, @@ -40,28 +69,72 @@ async def get_tokens(request: Request, @router.post("/{token_uid}/authorize", response_model=OCPIResponse) -async def authorize_token(request: Request, response: Response, - token_uid: CiString(36), token_type: TokenType = TokenType.rfid, - location_reference: LocationReference = None, - crud: Crud = Depends(get_crud), adapter: Adapter = Depends(get_adapter)): +async def authorize_token( + request: Request, + response: Response, + token_uid: CiString(36), # type: ignore + token_type: TokenType = TokenType.rfid, + location_reference: LocationReference = None, # type: ignore + crud: Crud = Depends(get_crud), + adapter: Adapter = Depends(get_adapter), +): + """ + Authorize Token. + + Authorizes a token based on the specified parameters. + + **Path parameters:** + - token_uid (str): The ID of the token to authorize (36 characters). + + **Query parameters:** + - token_type (TokenType): The type of the token (default=TokenType.rfid). + + **Request body:** + - location_reference (LocationReference): The location reference + for authorization (default=None). + + **Returns:** + The OCPIResponse containing the authorization result. + + **Raises:** + NotFoundOCPIError: If the token is not found. + """ + logger.info(f"Received request to authorize token with id `{token_uid}`") + logger.debug(f"Token type - `{token_type}`") + logger.debug(f"Location reference - `{location_reference}`") auth_token = get_auth_token(request) - try: - # check if token exists - await crud.get(ModuleID.tokens, RoleEnum.emsp, token_uid, - auth_token=auth_token, token_type=token_type, - version=VersionNumber.v_2_2_1) - location_reference = location_reference.dict() if location_reference else None + # check if token exists + token = await crud.get( + ModuleID.tokens, + RoleEnum.emsp, + token_uid, + auth_token=auth_token, + token_type=token_type, + version=VersionNumber.v_2_2_1, + ) + if token: + location_reference = ( + location_reference.dict() + if location_reference + else None # type: ignore + ) data = { - 'token_uid': token_uid, - 'token_type': token_type, - 'location_reference': location_reference + "token_uid": token_uid, + "token_type": token_type, + "location_reference": location_reference, } - authroization_result = await crud.do(ModuleID.tokens, RoleEnum.emsp, Action.authorize_token, data=data, - auth_token=auth_token) + authroization_result = await crud.do( + ModuleID.tokens, + RoleEnum.emsp, + Action.authorize_token, + data=data, + auth_token=auth_token, + ) # when the token information is not enough if not authroization_result: + logger.debug("Authorization result is null.") return OCPIResponse( data=[], **status.OCPI_2002_NOT_ENOUGH_INFORMATION, @@ -72,10 +145,5 @@ async def authorize_token(request: Request, response: Response, **status.OCPI_1000_GENERIC_SUCESS_CODE, ) - # when the token is not found - except NotFoundOCPIError: - response.status_code = http_status.HTTP_404_NOT_FOUND - return OCPIResponse( - data=[], - **status.OCPI_2004_UNKNOWN_TOKEN, - ) + logger.debug(f"Token with id `{token_uid}` was not found.") + raise NotFoundOCPIError diff --git a/py_ocpi/modules/tokens/v_2_2_1/enums.py b/py_ocpi/modules/tokens/v_2_2_1/enums.py index 204e1b9..b7f5b24 100644 --- a/py_ocpi/modules/tokens/v_2_2_1/enums.py +++ b/py_ocpi/modules/tokens/v_2_2_1/enums.py @@ -6,15 +6,16 @@ class AllowedType(str, Enum): https://github.com/ocpi/ocpi/blob/2.2.1/mod_tokens.asciidoc#141-allowedtype-enum """ # This Token is allowed to charge (at this location). - allowed = 'ALLOWED' + allowed = "ALLOWED" # This Token is blocked. - blocked = 'BLOCKED' + blocked = "BLOCKED" # This Token has expired. - expired = 'EXPIRED' - # This Token belongs to an account that has not enough credits to charge (at the given location). - no_credit = 'NO_CREDIT' + expired = "EXPIRED" + # This Token belongs to an account that has not enough credits to charge + # (at the given location). + no_credit = "NO_CREDIT" # Token is valid, but is not allowed to charge at the given location. - not_allowed = 'NOT_ALLOWED' + not_allowed = "NOT_ALLOWED" class TokenType(str, Enum): @@ -23,21 +24,21 @@ class TokenType(str, Enum): """ # One time use Token ID generated by a server (or App.) # The eMSP uses this to bind a Session to a customer, probably an app user. - ad_hoc_user = 'AD_HOC_USER' + ad_hoc_user = "AD_HOC_USER" # Token ID generated by a server (or App.) to identify a user of an App. # The same user uses the same Token for every Session. - app_user = 'APP_USER' + app_user = "APP_USER" # Other type of token - other = 'OTHER' + other = "OTHER" # RFID Token - rfid = 'RFID' + rfid = "RFID" class WhitelistType(str, Enum): """ https://github.com/ocpi/ocpi/blob/2.2.1/mod_tokens.asciidoc#145-whitelisttype-enum """ - always = 'ALWAYS' - allowed = 'ALLOWED' - allowed_offline = 'ALLOWED_OFFLINE' - never = 'NEVER' + always = "ALWAYS" + allowed = "ALLOWED" + allowed_offline = "ALLOWED_OFFLINE" + never = "NEVER" diff --git a/py_ocpi/modules/tokens/v_2_2_1/schemas.py b/py_ocpi/modules/tokens/v_2_2_1/schemas.py index f1f8eee..e3071eb 100644 --- a/py_ocpi/modules/tokens/v_2_2_1/schemas.py +++ b/py_ocpi/modules/tokens/v_2_2_1/schemas.py @@ -2,7 +2,11 @@ from pydantic import BaseModel from py_ocpi.core.data_types import String, CiString, DisplayText, DateTime -from py_ocpi.modules.tokens.v_2_2_1.enums import AllowedType, TokenType, WhitelistType +from py_ocpi.modules.tokens.v_2_2_1.enums import ( + AllowedType, + TokenType, + WhitelistType, +) from py_ocpi.modules.sessions.v_2_2_1.enums import ProfileType diff --git a/py_ocpi/modules/versions/api/__init__.py b/py_ocpi/modules/versions/api/__init__.py deleted file mode 100644 index b9dd260..0000000 --- a/py_ocpi/modules/versions/api/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .main import router -from .v_2_2_1 import router as versions_v_2_2_1_router diff --git a/py_ocpi/modules/versions/api/main.py b/py_ocpi/modules/versions/api/main.py deleted file mode 100644 index 58830a0..0000000 --- a/py_ocpi/modules/versions/api/main.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import APIRouter, Depends - -from py_ocpi.core import status -from py_ocpi.core.dependencies import get_versions as get_versions_ -from py_ocpi.core.schemas import OCPIResponse - -router = APIRouter() - - -@router.get("/versions", response_model=OCPIResponse) -async def get_versions(versions=Depends(get_versions_)): - return OCPIResponse( - data=versions, - **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 deleted file mode 100644 index 158f2cc..0000000 --- a/py_ocpi/modules/versions/api/v_2_2_1.py +++ /dev/null @@ -1,30 +0,0 @@ -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.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(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(), - **status.OCPI_1000_GENERIC_SUCESS_CODE, - ) diff --git a/py_ocpi/modules/versions/enums.py b/py_ocpi/modules/versions/enums.py index 67aaa2a..0d7d81c 100644 --- a/py_ocpi/modules/versions/enums.py +++ b/py_ocpi/modules/versions/enums.py @@ -5,23 +5,9 @@ class VersionNumber(str, Enum): """ https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#125-versionnumber-enum """ - v_2_0 = '2.0' - v_2_1 = '2.1' - v_2_1_1 = '2.1.1' - v_2_2 = '2.2' - v_2_2_1 = '2.2.1' - latest = '2.2.1' - - -class InterfaceRole(str, Enum): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#123-interfacerole-enum - """ - # Sender Interface implementation. - # Interface implemented by the owner of data, - # so the Receiver can Pull information from the data Sender/owner. - sender = 'SENDER' - # Receiver Interface implementation. - # Interface implemented by the receiver of data, - # so the Sender/owner can Push information to the Receiver. - receiver = 'RECEIVER' + v_2_0 = "2.0" + v_2_1 = "2.1" + v_2_1_1 = "2.1.1" + v_2_2 = "2.2" + v_2_2_1 = "2.2.1" + latest = "2.2.1" diff --git a/py_ocpi/modules/versions/main.py b/py_ocpi/modules/versions/main.py new file mode 100644 index 0000000..ac3a4db --- /dev/null +++ b/py_ocpi/modules/versions/main.py @@ -0,0 +1,53 @@ +from typing import Union + +from fastapi import ( + APIRouter, + Depends, + Request, + HTTPException, + status as fastapistatus, +) + +from py_ocpi.core.authentication.verifier import ( + VersionsAuthorizationVerifier, +) +from py_ocpi.core import status +from py_ocpi.core.config import logger +from py_ocpi.core.crud import Crud +from py_ocpi.core.dependencies import ( + get_versions as get_versions_, + get_crud, +) +from py_ocpi.core.schemas import OCPIResponse + + +router = APIRouter() +cred_dependency = VersionsAuthorizationVerifier(None) + + +@router.get("/versions", response_model=OCPIResponse) +async def get_versions( + request: Request, + versions=Depends(get_versions_), + crud: Crud = Depends(get_crud), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Get OCPI Versions. + + Retrieves a list of available OCPI versions. + + **Returns:** + The OCPIResponse containing a list of available OCPI versions. + """ + logger.info(f"Received request for version details: {request.url}") + if server_cred is None: + logger.debug("Unauthorized request.") + raise HTTPException( + fastapistatus.HTTP_401_UNAUTHORIZED, + "Unauthorized", + ) + return OCPIResponse( + data=versions, + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/versions/schemas.py b/py_ocpi/modules/versions/schemas.py index 455cc2a..e6c5956 100644 --- a/py_ocpi/modules/versions/schemas.py +++ b/py_ocpi/modules/versions/schemas.py @@ -1,10 +1,7 @@ -from typing import List - from pydantic import BaseModel -from py_ocpi.modules.versions.enums import VersionNumber, InterfaceRole +from py_ocpi.modules.versions.enums import VersionNumber from py_ocpi.core.data_types import URL -from py_ocpi.core.enums import ModuleID class Version(BaseModel): @@ -13,20 +10,3 @@ class Version(BaseModel): """ version: VersionNumber url: URL - - -class Endpoint(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#122-endpoint-class - """ - identifier: ModuleID - role: InterfaceRole - url: URL - - -class VersionDetail(BaseModel): - """ - https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#121-data - """ - version: VersionNumber - endpoints: List[Endpoint] diff --git a/py_ocpi/modules/versions/v_2_1_1/api/__init__.py b/py_ocpi/modules/versions/v_2_1_1/api/__init__.py new file mode 100644 index 0000000..51f1041 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_1_1/api/__init__.py @@ -0,0 +1 @@ +from .main import router diff --git a/py_ocpi/modules/versions/v_2_1_1/api/main.py b/py_ocpi/modules/versions/v_2_1_1/api/main.py new file mode 100644 index 0000000..12786d6 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_1_1/api/main.py @@ -0,0 +1,55 @@ +from typing import Union + +from fastapi import ( + APIRouter, + Depends, + Request, + HTTPException, + status as fastapistatus, +) + +from py_ocpi.core.authentication.verifier import ( + VersionsAuthorizationVerifier, +) +from py_ocpi.core.crud import Crud +from py_ocpi.core import status +from py_ocpi.core.config import logger +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.dependencies import get_endpoints, get_crud + +from py_ocpi.modules.versions.v_2_1_1.schemas import ( + VersionDetail, + VersionNumber, +) + +router = APIRouter() +cred_dependency = VersionsAuthorizationVerifier(VersionNumber.v_2_1_1) + + +@router.get("/2.1.1/details", response_model=OCPIResponse) +async def get_version_details( + request: Request, + endpoints=Depends(get_endpoints), + crud: Crud = Depends(get_crud), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Get Version Details. + + Retrieves details of the OCPI version 2.1.1. + + **Returns:** + The OCPIResponse containing details of the OCPI version 2.1.1. + """ + logger.info(f"Received request for version details: {request.url}") + if server_cred is None: + logger.debug("Unauthorized request.") + raise HTTPException(fastapistatus.HTTP_401_UNAUTHORIZED, "Unauthorized") + + return OCPIResponse( + data=VersionDetail( + version=VersionNumber.v_2_1_1, + endpoints=endpoints[VersionNumber.v_2_1_1], + ).dict(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/versions/v_2_1_1/enums.py b/py_ocpi/modules/versions/v_2_1_1/enums.py new file mode 100644 index 0000000..13999e8 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_1_1/enums.py @@ -0,0 +1 @@ +from py_ocpi.modules.versions.enums import * # noqa diff --git a/py_ocpi/modules/versions/v_2_1_1/schemas.py b/py_ocpi/modules/versions/v_2_1_1/schemas.py new file mode 100644 index 0000000..ea65819 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_1_1/schemas.py @@ -0,0 +1,25 @@ +from typing import List + +from pydantic import BaseModel + +from py_ocpi.modules.versions.v_2_1_1.enums import VersionNumber +from py_ocpi.core.data_types import URL +from py_ocpi.core.enums import ModuleID + + +class Endpoint(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/version_information_endpoint.md#endpoint-class + """ + + identifier: ModuleID + url: URL + + +class VersionDetail(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/version_information_endpoint.md#data-1 + """ + + version: VersionNumber + endpoints: List[Endpoint] diff --git a/py_ocpi/modules/versions/v_2_2_1/api/__init__.py b/py_ocpi/modules/versions/v_2_2_1/api/__init__.py new file mode 100644 index 0000000..51f1041 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_2_1/api/__init__.py @@ -0,0 +1 @@ +from .main import router diff --git a/py_ocpi/modules/versions/v_2_2_1/api/main.py b/py_ocpi/modules/versions/v_2_2_1/api/main.py new file mode 100644 index 0000000..abe21a4 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_2_1/api/main.py @@ -0,0 +1,55 @@ +from typing import Union + +from fastapi import ( + APIRouter, + Depends, + Request, + HTTPException, + status as fastapistatus, +) + +from py_ocpi.core.authentication.verifier import ( + VersionsAuthorizationVerifier, +) +from py_ocpi.core.crud import Crud +from py_ocpi.core import status +from py_ocpi.core.config import logger +from py_ocpi.core.schemas import OCPIResponse +from py_ocpi.core.dependencies import get_endpoints, get_crud + +from py_ocpi.modules.versions.v_2_2_1.schemas import ( + VersionDetail, + VersionNumber, +) + +router = APIRouter() +cred_dependency = VersionsAuthorizationVerifier(VersionNumber.v_2_2_1) + + +@router.get("/2.2.1/details", response_model=OCPIResponse) +async def get_version_details( + request: Request, + endpoints=Depends(get_endpoints), + crud: Crud = Depends(get_crud), + server_cred: Union[str, dict, None] = Depends(cred_dependency), +): + """ + Get Version Details. + + Retrieves details of the OCPI version 2.2.1. + + **Returns:** + The OCPIResponse containing details of the OCPI version 2.2.1. + """ + logger.info(f"Received request for version details: {request.url}") + if server_cred is None: + logger.debug("Unauthorized request.") + 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(), + **status.OCPI_1000_GENERIC_SUCESS_CODE, + ) diff --git a/py_ocpi/modules/versions/v_2_2_1/enums.py b/py_ocpi/modules/versions/v_2_2_1/enums.py new file mode 100644 index 0000000..942a093 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_2_1/enums.py @@ -0,0 +1,16 @@ +from py_ocpi.modules.versions.enums import * # noqa + + +class InterfaceRole(str, Enum): # noqa + """ + https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#123-interfacerole-enum + """ + + # Sender Interface implementation. + # Interface implemented by the owner of data, + # so the Receiver can Pull information from the data Sender/owner. + sender = "SENDER" + # Receiver Interface implementation. + # Interface implemented by the receiver of data, + # so the Sender/owner can Push information to the Receiver. + receiver = "RECEIVER" diff --git a/py_ocpi/modules/versions/v_2_2_1/schemas.py b/py_ocpi/modules/versions/v_2_2_1/schemas.py new file mode 100644 index 0000000..691b5f3 --- /dev/null +++ b/py_ocpi/modules/versions/v_2_2_1/schemas.py @@ -0,0 +1,26 @@ +from typing import List + +from pydantic import BaseModel + +from py_ocpi.modules.versions.v_2_2_1.enums import InterfaceRole, VersionNumber +from py_ocpi.core.data_types import URL +from py_ocpi.core.enums import ModuleID + + +class Endpoint(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#122-endpoint-class + """ + + identifier: ModuleID + role: InterfaceRole + url: URL + + +class VersionDetail(BaseModel): + """ + https://github.com/ocpi/ocpi/blob/2.2.1/version_information_endpoint.asciidoc#121-data + """ + + version: VersionNumber + endpoints: List[Endpoint] diff --git a/py_ocpi/routers/__init__.py b/py_ocpi/routers/__init__.py index 4fee20a..ee7ebce 100644 --- a/py_ocpi/routers/__init__.py +++ b/py_ocpi/routers/__init__.py @@ -1,2 +1,4 @@ from .v_2_2_1.cpo import router as v_2_2_1_cpo_router from .v_2_2_1.emsp import router as v_2_2_1_emsp_router +from .v_2_1_1.cpo import router as v_2_1_1_cpo_router +from .v_2_1_1.emsp import router as v_2_1_1_emsp_router diff --git a/py_ocpi/routers/v_2_1_1/cpo.py b/py_ocpi/routers/v_2_1_1/cpo.py new file mode 100644 index 0000000..4a3d24a --- /dev/null +++ b/py_ocpi/routers/v_2_1_1/cpo.py @@ -0,0 +1,33 @@ +from py_ocpi.core.enums import ModuleID + +from py_ocpi.modules.credentials.v_2_1_1.api import ( + cpo_router as credentials_cpo_2_1_1_router, +) +from py_ocpi.modules.locations.v_2_1_1.api import ( + cpo_router as locations_cpo_2_1_1_router, +) +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, +) +from py_ocpi.modules.sessions.v_2_1_1.api import ( + cpo_router as sessions_cpo_2_1_1_router, +) +from py_ocpi.modules.tokens.v_2_1_1.api import ( + cpo_router as tokens_cpo_2_1_1_router, +) +from py_ocpi.modules.commands.v_2_1_1.api import ( + cpo_router as commands_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, + ModuleID.sessions: sessions_cpo_2_1_1_router, + ModuleID.tokens: tokens_cpo_2_1_1_router, + ModuleID.commands: commands_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 new file mode 100644 index 0000000..0748858 --- /dev/null +++ b/py_ocpi/routers/v_2_1_1/emsp.py @@ -0,0 +1,33 @@ +from py_ocpi.core.enums import ModuleID + +from py_ocpi.modules.credentials.v_2_1_1.api import ( + emsp_router as credentials_emsp_2_1_1_router, +) +from py_ocpi.modules.locations.v_2_1_1.api import ( + emsp_router as locations_emsp_2_1_1_router, +) +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, +) +from py_ocpi.modules.sessions.v_2_1_1.api import ( + emsp_router as sessions_emsp_2_1_1_router, +) +from py_ocpi.modules.tokens.v_2_1_1.api import ( + emsp_router as tokens_emsp_2_1_1_router, +) +from py_ocpi.modules.commands.v_2_1_1.api import ( + emsp_router as commands_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, + ModuleID.sessions: sessions_emsp_2_1_1_router, + ModuleID.tokens: tokens_emsp_2_1_1_router, + ModuleID.commands: commands_emsp_2_1_1_router, +} diff --git a/py_ocpi/routers/v_2_2_1/cpo.py b/py_ocpi/routers/v_2_2_1/cpo.py index 3ca8e3a..ad94238 100644 --- a/py_ocpi/routers/v_2_2_1/cpo.py +++ b/py_ocpi/routers/v_2_2_1/cpo.py @@ -1,34 +1,42 @@ -from fastapi import APIRouter +from py_ocpi.core.enums import ModuleID -from py_ocpi.modules.credentials.v_2_2_1.api import cpo_router as credentials_cpo_2_2_1_router -from py_ocpi.modules.locations.v_2_2_1.api import cpo_router as locations_cpo_2_2_1_router -from py_ocpi.modules.sessions.v_2_2_1.api import cpo_router as sessions_cpo_2_2_1_router -from py_ocpi.modules.commands.v_2_2_1.api import cpo_router as commands_cpo_2_2_1_router -from py_ocpi.modules.tariffs.v_2_2_1.api import cpo_router as tariffs_cpo_2_2_1_router -from py_ocpi.modules.tokens.v_2_2_1.api import cpo_router as tokens_cpo_2_2_1_router -from py_ocpi.modules.cdrs.v_2_2_1.api import cpo_router as cdrs_cpo_2_2_1_router - - -router = APIRouter( +from py_ocpi.modules.credentials.v_2_2_1.api import ( + cpo_router as credentials_cpo_2_2_1_router, +) +from py_ocpi.modules.locations.v_2_2_1.api import ( + cpo_router as locations_cpo_2_2_1_router, ) -router.include_router( - locations_cpo_2_2_1_router +from py_ocpi.modules.sessions.v_2_2_1.api import ( + cpo_router as sessions_cpo_2_2_1_router, ) -router.include_router( - credentials_cpo_2_2_1_router +from py_ocpi.modules.commands.v_2_2_1.api import ( + cpo_router as commands_cpo_2_2_1_router, ) -router.include_router( - sessions_cpo_2_2_1_router +from py_ocpi.modules.tariffs.v_2_2_1.api import ( + cpo_router as tariffs_cpo_2_2_1_router, ) -router.include_router( - commands_cpo_2_2_1_router +from py_ocpi.modules.tokens.v_2_2_1.api import ( + cpo_router as tokens_cpo_2_2_1_router, ) -router.include_router( - tariffs_cpo_2_2_1_router +from py_ocpi.modules.cdrs.v_2_2_1.api import ( + cpo_router as cdrs_cpo_2_2_1_router, ) -router.include_router( - tokens_cpo_2_2_1_router +from py_ocpi.modules.hubclientinfo.v_2_2_1.api import ( + cpo_router as hubclientinfo_cpo_2_2_1_router, ) -router.include_router( - cdrs_cpo_2_2_1_router +from py_ocpi.modules.chargingprofiles.v_2_2_1.api import ( + cpo_router as chargingprofiles_cpo_2_2_1_router, ) + + +router = { + ModuleID.locations: locations_cpo_2_2_1_router, + ModuleID.credentials_and_registration: credentials_cpo_2_2_1_router, + ModuleID.sessions: sessions_cpo_2_2_1_router, + ModuleID.commands: commands_cpo_2_2_1_router, + ModuleID.tariffs: tariffs_cpo_2_2_1_router, + ModuleID.tokens: tokens_cpo_2_2_1_router, + ModuleID.cdrs: cdrs_cpo_2_2_1_router, + ModuleID.hub_client_info: hubclientinfo_cpo_2_2_1_router, + ModuleID.charging_profile: chargingprofiles_cpo_2_2_1_router, +} diff --git a/py_ocpi/routers/v_2_2_1/emsp.py b/py_ocpi/routers/v_2_2_1/emsp.py index 46560f0..572bbaf 100644 --- a/py_ocpi/routers/v_2_2_1/emsp.py +++ b/py_ocpi/routers/v_2_2_1/emsp.py @@ -1,34 +1,42 @@ -from fastapi import APIRouter +from py_ocpi.core.enums import ModuleID -from py_ocpi.modules.credentials.v_2_2_1.api import emsp_router as credentials_emsp_2_2_1_router -from py_ocpi.modules.locations.v_2_2_1.api import emsp_router as locations_emsp_2_2_1_router -from py_ocpi.modules.sessions.v_2_2_1.api import emsp_router as sessions_emsp_2_2_1_router -from py_ocpi.modules.cdrs.v_2_2_1.api import emsp_router as cdrs_emsp_2_2_1_router -from py_ocpi.modules.tariffs.v_2_2_1.api import emsp_router as tariffs_emsp_2_2_1_router -from py_ocpi.modules.commands.v_2_2_1.api import emsp_router as commands_emsp_2_2_1_router -from py_ocpi.modules.tokens.v_2_2_1.api import emsp_router as tokens_emsp_2_2_1_router - - -router = APIRouter( +from py_ocpi.modules.credentials.v_2_2_1.api import ( + emsp_router as credentials_emsp_2_2_1_router, +) +from py_ocpi.modules.locations.v_2_2_1.api import ( + emsp_router as locations_emsp_2_2_1_router, ) -router.include_router( - locations_emsp_2_2_1_router +from py_ocpi.modules.sessions.v_2_2_1.api import ( + emsp_router as sessions_emsp_2_2_1_router, ) -router.include_router( - credentials_emsp_2_2_1_router +from py_ocpi.modules.cdrs.v_2_2_1.api import ( + emsp_router as cdrs_emsp_2_2_1_router, ) -router.include_router( - sessions_emsp_2_2_1_router +from py_ocpi.modules.tariffs.v_2_2_1.api import ( + emsp_router as tariffs_emsp_2_2_1_router, ) -router.include_router( - cdrs_emsp_2_2_1_router +from py_ocpi.modules.commands.v_2_2_1.api import ( + emsp_router as commands_emsp_2_2_1_router, ) -router.include_router( - tariffs_emsp_2_2_1_router +from py_ocpi.modules.tokens.v_2_2_1.api import ( + emsp_router as tokens_emsp_2_2_1_router, ) -router.include_router( - commands_emsp_2_2_1_router +from py_ocpi.modules.hubclientinfo.v_2_2_1.api import ( + emsp_router as hubclientinfo_emsp_2_2_1_router, ) -router.include_router( - tokens_emsp_2_2_1_router +from py_ocpi.modules.chargingprofiles.v_2_2_1.api import ( + emsp_router as chargingprofiles_emsp_2_2_1_router, ) + + +router = { + ModuleID.locations: locations_emsp_2_2_1_router, + ModuleID.credentials_and_registration: credentials_emsp_2_2_1_router, + ModuleID.sessions: sessions_emsp_2_2_1_router, + ModuleID.commands: commands_emsp_2_2_1_router, + ModuleID.tariffs: tariffs_emsp_2_2_1_router, + ModuleID.tokens: tokens_emsp_2_2_1_router, + ModuleID.cdrs: cdrs_emsp_2_2_1_router, + ModuleID.hub_client_info: hubclientinfo_emsp_2_2_1_router, + ModuleID.charging_profile: chargingprofiles_emsp_2_2_1_router, +} diff --git a/tests/test_dependency_injection.py b/tests/test_dependency_injection.py index 0830320..6e84f96 100644 --- a/tests/test_dependency_injection.py +++ b/tests/test_dependency_injection.py @@ -6,16 +6,56 @@ from py_ocpi.core import enums from py_ocpi.modules.versions.enums import VersionNumber +from tests.test_modules.utils import ( + ClientAuthenticator, + ENCODED_AUTH_TOKEN, + AUTH_TOKEN, +) -def test_inject_dependency(): + +def test_inject_dependency_v_2_2_1(): + crud = AsyncMock() + crud.list.return_value = [], 0, True + + adapter = MagicMock() + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=crud, + adapter=adapter, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) + + client = TestClient(app) + client.get( + "/ocpi/cpo/2.2.1/locations", + headers={"Authorization": f"Token {ENCODED_AUTH_TOKEN}"}, + ) + + crud.list.assert_awaited_once() + + +def test_inject_dependency_v_2_1_1(): crud = AsyncMock() crud.list.return_value = [], 0, True adapter = MagicMock() - app = get_application([VersionNumber.v_2_2_1], [enums.RoleEnum.cpo], crud, adapter) + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=crud, + adapter=adapter, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) client = TestClient(app) - client.get('/ocpi/cpo/2.2.1/locations') + client.get( + "/ocpi/cpo/2.1.1/locations", + headers={"Authorization": f"Token {AUTH_TOKEN}"}, + ) crud.list.assert_awaited_once() diff --git a/tests/test_main.py b/tests/test_main.py index 9ff145a..5e897a8 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,8 @@ from py_ocpi.core import enums from py_ocpi.modules.versions.enums import VersionNumber +from tests.test_modules.utils import ClientAuthenticator + def test_get_application(): class Crud: @@ -10,6 +12,13 @@ class Crud: class Adapter: ... - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + modules=[], + adapter=Adapter, + authenticator=ClientAuthenticator, + ) - assert app.url_path_for('get_versions') == "/ocpi/versions" + assert app.url_path_for("get_versions") == "/ocpi/versions" diff --git a/tests/test_modules/__init__.py b/tests/test_modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/mocks/async_client.py b/tests/test_modules/mocks/async_client.py index 4d64f29..d419840 100644 --- a/tests/test_modules/mocks/async_client.py +++ b/tests/test_modules/mocks/async_client.py @@ -1,21 +1,19 @@ 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.core.enums import RoleEnum, ModuleID from py_ocpi.modules.versions.enums import VersionNumber -from py_ocpi.modules.versions.schemas import VersionDetail +from py_ocpi.modules.versions.v_2_2_1.schemas import VersionDetail fake_endpoints_data = { - 'data': [ - VersionDetail( - version=VersionNumber.v_2_2_1, - endpoints=ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.cpo] - ).dict(), - ], + "data": VersionDetail( + version=VersionNumber.v_2_2_1, + endpoints=[ + ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.cpo][ModuleID.locations] + ], + ).dict(), } -fake_versions_data = { - 'data': get_versions() -} +fake_versions_data = {"data": get_versions()} class MockResponse: @@ -29,9 +27,10 @@ def json(self): # Connector mocks + class MockAsyncClientVersionsAndEndpoints: async def get(url, headers): - if url == 'versions_url': + if url == "versions_url": return MockResponse(fake_versions_data, 200) else: return MockResponse(fake_endpoints_data, 200) @@ -44,7 +43,6 @@ async def send(request): class MockAsyncClientGeneratorVersionsAndEndpoints: - async def __aenter__(self): return MockAsyncClientVersionsAndEndpoints diff --git a/tests/test_modules/test_cdrs.py b/tests/test_modules/test_cdrs.py deleted file mode 100644 index 015d73e..0000000 --- a/tests/test_modules/test_cdrs.py +++ /dev/null @@ -1,122 +0,0 @@ -from uuid import uuid4 - -from fastapi.testclient import TestClient - -from py_ocpi import get_application -from py_ocpi.core import enums -from py_ocpi.modules.locations.v_2_2_1.schemas import ConnectorType, ConnectorFormat, PowerType -from py_ocpi.modules.cdrs.v_2_2_1.schemas import TokenType, Cdr -from py_ocpi.modules.cdrs.v_2_2_1.enums import AuthMethod, CdrDimensionType -from py_ocpi.modules.versions.enums import VersionNumber - -CDRS = [ - { - 'country_code': 'us', - 'party_id': 'AAA', - 'id': str(uuid4()), - 'start_date_time': '2022-01-02 00:00:00+00:00', - 'end_date_time': '2022-01-02 00:05:00+00:00', - 'cdr_token': { - 'country_code': 'us', - 'party_id': 'AAA', - 'uid': str(uuid4()), - 'type': TokenType.rfid, - 'contract_id': str(uuid4()) - }, - 'auth_method': AuthMethod.auth_request, - 'cdr_location': { - 'id': str(uuid4()), - 'name': 'name', - 'address': 'address', - 'city': 'city', - 'postal_code': '111111', - 'state': 'state', - 'country': 'USA', - 'coordinates': { - 'latitude': 'latitude', - 'longitude': 'longitude', - }, - 'evse_id': str(uuid4()), - 'connector_id': str(uuid4()), - 'connector_standard': ConnectorType.tesla_r, - 'connector_format': ConnectorFormat.cable, - 'connector_power_type': PowerType.dc - }, - 'currency': 'MYR', - 'charging_periods': [ - { - 'start_date_time': '2022-01-02 00:00:00+00:00', - 'dimensions': [ - { - 'type': CdrDimensionType.power, - 'volume': 10 - } - ] - } - ], - 'total_cost': { - 'excl_vat': 10.0000, - 'incl_vat': 10.2500 - }, - 'total_energy': 50, - 'total_time': 500, - 'last_updated': '2022-01-02 00:00:00+00:00' - } -] - - -class Crud: - - @classmethod - async def list(cls, module: enums.ModuleID, role: enums.RoleEnum, filters: dict, *args, **kwargs) -> list: - return CDRS, 1, True - - @classmethod - async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs): - return CDRS[0] - - @classmethod - async def create(cls, module: enums.ModuleID, role: enums.RoleEnum, data: dict, *args, **kwargs): - return data - - -class Adapter: - @classmethod - def cdr_adapter(cls, data, version: VersionNumber = VersionNumber.latest) -> Cdr: - return Cdr(**data) - - -def test_cpo_get_cdrs_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get('/ocpi/cpo/2.2.1/cdrs') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['id'] == CDRS[0]["id"] - - -def test_emsp_get_cdr_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/emsp/2.2.1/cdrs/{CDRS[0]["id"]}') - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == CDRS[0]["id"] - - -def test_emsp_add_cdr_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - data = CDRS[0] - - client = TestClient(app) - response = client.post('/ocpi/emsp/2.2.1/cdrs/', json=data) - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == CDRS[0]["id"] - assert response.headers['Location'] is not None diff --git a/tests/test_modules/test_commands.py b/tests/test_modules/test_commands.py deleted file mode 100644 index 4ddf0e1..0000000 --- a/tests/test_modules/test_commands.py +++ /dev/null @@ -1,175 +0,0 @@ -import datetime -from uuid import uuid4 - -from fastapi.testclient import TestClient - -from py_ocpi import get_application -from py_ocpi.core import enums -from py_ocpi.core.exceptions import NotFoundOCPIError -from py_ocpi.modules.tokens.v_2_2_1.enums import TokenType, WhitelistType -from py_ocpi.modules.commands.v_2_2_1.enums import CommandType, CommandResponseType, CommandResultType -from py_ocpi.modules.commands.v_2_2_1.schemas import CommandResponse, CommandResult -from py_ocpi.modules.versions.enums import VersionNumber - -COMMAND_RESPONSE = { - 'result': CommandResponseType.accepted, - 'timeout': 30 -} - -COMMAND_RESULT = { - 'result': CommandResultType.accepted, -} - - -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 - async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs) -> dict: - return COMMAND_RESULT - - @classmethod - async def update(cls, module: enums.ModuleID, role: enums.RoleEnum, data: dict, id, *args, **kwargs): - ... - - -class Adapter: - @classmethod - def command_response_adapter(cls, data, version: VersionNumber = VersionNumber.latest): - return CommandResponse(**data) - - @classmethod - def command_result_adapter(cls, data, version: VersionNumber = VersionNumber.latest): - return CommandResult(**data) - - -def test_cpo_receive_command_start_session_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - data = { - 'response_url': 'https://dummy.restapiexample.com/api/v1/create', - 'token': { - 'country_code': 'us', - 'party_id': 'AAA', - 'uid': str(uuid4()), - 'type': TokenType.rfid, - 'contract_id': str(uuid4()), - 'issuer': 'company', - 'valid': True, - 'whitelist': WhitelistType.always, - 'last_updated': '2022-01-02 00:00:00+00:00' - - }, - 'location_id': str(uuid4()) - } - - client = TestClient(app) - 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 - assert response.json()['data'][0]['result'] == COMMAND_RESPONSE["result"] - - -def test_cpo_receive_command_stop_session_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - data = { - 'response_url': 'https://dummy.restapiexample.com/api/v1/create', - 'session_id': str(uuid4()) - } - - client = TestClient(app) - 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 - assert response.json()['data'][0]['result'] == COMMAND_RESPONSE["result"] - - -def test_cpo_receive_command_reserve_now_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - data = { - 'response_url': 'https://dummy.restapiexample.com/api/v1/create', - 'token': { - 'country_code': 'us', - 'party_id': 'AAA', - 'uid': str(uuid4()), - 'type': TokenType.rfid, - 'contract_id': str(uuid4()), - 'issuer': 'company', - 'valid': True, - 'whitelist': WhitelistType.always, - 'last_updated': '2022-01-02 00:00:00+00:00' - - }, - 'expiry_date': str(datetime.datetime.now() + datetime.timedelta(days=1)), - 'reservation_id': str(uuid4()), - 'location_id': str(uuid4()) - } - - client = TestClient(app) - 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 - assert response.json()['data'][0]['result'] == COMMAND_RESPONSE["result"] - - -def test_cpo_receive_command_reserve_now_unknown_location_v_2_2_1(): - - @classmethod - async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs) -> dict: - if module == enums.ModuleID.commands: - return COMMAND_RESULT - if module == enums.ModuleID.locations: - raise NotFoundOCPIError() - _get = Crud.get - Crud.get = get - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - data = { - 'response_url': 'https://dummy.restapiexample.com/api/v1/create', - 'token': { - 'country_code': 'us', - 'party_id': 'AAA', - 'uid': str(uuid4()), - 'type': TokenType.rfid, - 'contract_id': str(uuid4()), - 'issuer': 'company', - 'valid': True, - 'whitelist': WhitelistType.always, - 'last_updated': '2022-01-02 00:00:00+00:00' - - }, - 'expiry_date': str(datetime.datetime.now() + datetime.timedelta(days=1)), - 'reservation_id': str(uuid4()), - 'location_id': str(uuid4()) - } - - client = TestClient(app) - 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 - - # revert Crud changes - Crud.get = _get - - -def test_emsp_receive_command_result_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.post('/ocpi/emsp/2.2.1/commands/1234', json=COMMAND_RESPONSE) - - assert response.status_code == 200 diff --git a/tests/test_modules/test_credentials.py b/tests/test_modules/test_credentials.py deleted file mode 100644 index d11c51b..0000000 --- a/tests/test_modules/test_credentials.py +++ /dev/null @@ -1,124 +0,0 @@ -import functools -from uuid import uuid4 -from unittest.mock import patch -from typing import Any - -import pytest -from fastapi.testclient import TestClient -from httpx import AsyncClient - -from py_ocpi import get_application -from py_ocpi.core import enums -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 -from py_ocpi.modules.versions.enums import VersionNumber -from py_ocpi.modules.versions.schemas import Version - -CREDENTIALS_TOKEN_GET = { - 'url': 'url', - 'roles': [{ - 'role': enums.RoleEnum.emsp, - 'business_details': { - 'name': 'name', - }, - 'party_id': 'JOM', - 'country_code': 'MY' - }] -} - -CREDENTIALS_TOKEN_CREATE = { - 'token': str(uuid4()), - 'url': '/ocpi/versions', - 'roles': [{ - 'role': enums.RoleEnum.emsp, - 'business_details': { - 'name': 'name', - }, - 'party_id': 'JOM', - 'country_code': 'MY' - }] -} - - -def partial_class(cls, *args, **kwds): - - class NewCls(cls): - __init__ = functools.partialmethod(cls.__init__, *args, **kwds) - - return NewCls - - -class Crud: - @classmethod - async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs): - if id == CREDENTIALS_TOKEN_CREATE['token']: - return None - return dict(CREDENTIALS_TOKEN_GET, **{'token': id}) - - @classmethod - async def create(cls, module: enums.ModuleID, data, operation, *args, **kwargs): - if operation == 'credentials': - return None - return CREDENTIALS_TOKEN_CREATE - - @classmethod - async def do(cls, module: enums.ModuleID, role: enums.RoleEnum, action: enums.Action, *args, - data: dict = None, **kwargs): - return None - - -class Adapter: - @classmethod - def credentials_adapter(cls, data, version: VersionNumber = VersionNumber.latest) -> Credentials: - return Credentials(**data) - - -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 {encode_string_base64(token)}' - } - - client = TestClient(app) - response = client.get('/ocpi/cpo/2.2.1/credentials', headers=header) - - assert response.status_code == 200 - 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): - 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.value}/details') - ).dict() - ] - - app_1.dependency_overrides[get_versions] = 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], 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 response.json()['data']['token'] == CREDENTIALS_TOKEN_CREATE['token'] diff --git a/tests/test_modules/test_locations.py b/tests/test_modules/test_locations.py deleted file mode 100644 index f5a5b67..0000000 --- a/tests/test_modules/test_locations.py +++ /dev/null @@ -1,381 +0,0 @@ -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.locations.v_2_2_1.schemas import Location -from py_ocpi.modules.versions.enums import VersionNumber - - -LOCATIONS = [ - { - 'country_code': 'us', - 'party_id': 'AAA', - 'id': str(uuid4()), - 'publish': True, - 'publish_allowed_to': [ - { - 'uid': str(uuid4()), - 'type': 'APP_USER', - 'visual_number': '1', - 'issuer': 'issuer', - 'group_id': 'group_id', - }, - ], - 'name': 'name', - 'address': 'address', - 'city': 'city', - 'postal_code': '111111', - 'state': 'state', - 'country': 'USA', - 'coordinates': { - 'latitude': 'latitude', - 'longitude': 'longitude', - }, - 'related_locations': [ - { - 'latitude': 'latitude', - 'longitude': 'longitude', - 'name': { - 'language': 'en', - 'text': 'name' - } - }, - ], - 'parking_type': 'ON_STREET', - 'evses': [ - { - 'uid': str(uuid4()), - 'evse_id': str(uuid4()), - 'status': 'AVAILABLE', - 'status_schedule': { - 'period_begin': '2022-01-01T00:00:00+00:00', - 'period_end': '2022-01-01T00:00:00+00:00', - 'status': 'AVAILABLE' - }, - 'capabilities': [ - 'CREDIT_CARD_PAYABLE', - ], - 'connectors': [ - { - 'id': str(uuid4()), - 'standard': 'DOMESTIC_A', - 'format': 'SOCKET', - 'power_type': 'DC', - 'max_voltage': 100, - 'max_amperage': 100, - 'max_electric_power': 100, - 'tariff_ids': [str(uuid4()), ], - 'terms_and_conditions': 'https://www.example.com', - 'last_updated': '2022-01-01T00:00:00+00:00', - } - ], - 'floor_level': '3', - 'coordinates': { - 'latitude': 'latitude', - 'longitude': 'longitude', - }, - 'physical_reference': 'pr', - 'directions': [ - { - 'language': 'en', - 'text': 'directions' - }, - ], - 'parking_restrictions': ['EV_ONLY', ], - 'images': [ - { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - }, - ], - 'last_updated': '2022-01-01T00:00:00+00:00' - } - ], - 'directions': [ - { - 'language': 'en', - 'text': 'directions' - }, - ], - 'operator': { - 'name': 'name', - 'website': 'https://www.example.com', - 'logo': { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - } - }, - 'suboperator': { - 'name': 'name', - 'website': 'https://www.example.com', - 'logo': { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - } - }, - 'owner': { - 'name': 'name', - 'website': 'https://www.example.com', - 'logo': { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - } - }, - 'facilities': ['MALL'], - 'time_zone': 'UTC+2', - 'opening_times': { - 'twentyfourseven': True, - 'regular_hours': [ - { - 'weekday': 1, - 'period_begin': '8:00', - 'period_end': '22:00', - }, - { - 'weekday': 2, - 'period_begin': '8:00', - 'period_end': '22:00', - }, - ], - 'exceptional_openings': [ - { - 'period_begin': '2022-01-01T00:00:00+00:00', - 'period_end': '2022-01-02T00:00:00+00:00', - }, - ], - 'exceptional_closings': [], - }, - 'charging_when_closed': False, - 'images': [ - { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - }, - ], - 'energy_mix': { - 'is_green_energy': True, - 'energy_sources': [ - { - 'source': 'SOLAR', - 'percentage': 100 - }, - ], - 'supplier_name': 'supplier_name', - 'energy_product_name': 'energy_product_name' - }, - '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 LOCATIONS[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 list(cls, module: enums.ModuleID, role: enums.RoleEnum, filters: dict, *args, **kwargs) -> list: - return LOCATIONS, 1, True - - -class Adapter: - @classmethod - def location_adapter(cls, data, version: VersionNumber = VersionNumber.latest) -> Location: - return Location(**data) - - -def test_cpo_get_locations_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get('/ocpi/cpo/2.2.1/locations') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['id'] == LOCATIONS[0]["id"] - - -def test_cpo_get_location_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/cpo/2.2.1/locations/{LOCATIONS[0]["id"]}') - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == LOCATIONS[0]["id"] - - -def test_cpo_get_evse_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/cpo/2.2.1/locations/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['uid'] == LOCATIONS[0]["evses"][0]["uid"] - - -def test_cpo_get_connector_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/cpo/2.2.1/locations/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}' - f'/{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['id'] == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] - - -def test_emsp_get_location_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}') - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == LOCATIONS[0]["id"] - - -def test_emsp_get_evse_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['uid'] == LOCATIONS[0]["evses"][0]["uid"] - - -def test_emsp_get_connector_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}' - f'/{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['id'] == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] - - -def test_emsp_add_location_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.put(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}', json=LOCATIONS[0]) - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == LOCATIONS[0]["id"] - - -def test_emsp_add_evse_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.put(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}', json=LOCATIONS[0]["evses"][0]) - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['uid'] == LOCATIONS[0]["evses"][0]["uid"] - - -def test_emsp_add_connector_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.put(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}' - f'/{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}', - json=LOCATIONS[0]["evses"][0]["connectors"][0]) - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['id'] == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] - - -def test_emsp_patch_location_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - patch_data = {'id': str(uuid4())} - client = TestClient(app) - response = client.patch(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}', json=patch_data) - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == patch_data["id"] - - -def test_emsp_patch_evse_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - patch_data = {'uid': str(uuid4())} - client = TestClient(app) - response = client.patch(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}', json=patch_data) - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['uid'] == patch_data["uid"] - - -def test_emsp_patch_connector_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - patch_data = {'id': str(uuid4())} - client = TestClient(app) - response = client.patch(f'/ocpi/emsp/2.2.1/locations/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}' - f'/{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}', json=patch_data) - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['id'] == patch_data["id"] diff --git a/tests/test_modules/test_sessions.py b/tests/test_modules/test_sessions.py deleted file mode 100644 index 9f51b47..0000000 --- a/tests/test_modules/test_sessions.py +++ /dev/null @@ -1,144 +0,0 @@ -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.cdrs.v_2_2_1.schemas import TokenType -from py_ocpi.modules.cdrs.v_2_2_1.enums import AuthMethod, CdrDimensionType -from py_ocpi.modules.sessions.v_2_2_1.schemas import Session, ChargingPreferences -from py_ocpi.modules.sessions.v_2_2_1.enums import SessionStatus, ProfileType -from py_ocpi.modules.versions.enums import VersionNumber - -SESSIONS = [ - { - 'country_code': 'us', - 'party_id': 'AAA', - 'id': str(uuid4()), - 'start_date_time': '2022-01-02 00:00:00+00:00', - 'end_date_time': '2022-01-02 00:05:00+00:00', - 'kwh': 100, - 'cdr_token': { - 'country_code': 'us', - 'party_id': 'AAA', - 'uid': str(uuid4()), - 'type': TokenType.rfid, - 'contract_id': str(uuid4()) - }, - 'auth_method': AuthMethod.auth_request, - 'location_id': str(uuid4()), - 'evse_uid': str(uuid4()), - 'connector_id': str(uuid4()), - 'currency': 'MYR', - 'charging_periods': [ - { - 'start_date_time': '2022-01-02 00:00:00+00:00', - 'dimensions': [ - { - 'type': CdrDimensionType.power, - 'volume': 10 - } - ] - } - ], - 'total_cost': { - 'excl_vat': 10.0000, - 'incl_vat': 10.2500 - }, - 'status': SessionStatus.active, - 'last_updated': '2022-01-02 00:00:00+00:00' - } -] - -CHARGING_PREFERENCES = { - 'profile_type': ProfileType.fast, - 'departure_time': '2022-01-02 00:00:00+00:00', - 'energy_need': 100 -} - - -class Crud: - - @classmethod - async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs): - return SESSIONS[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 list(cls, module: enums.ModuleID, role: enums.RoleEnum, filters: dict, *args, **kwargs) -> list: - return SESSIONS, 1, True - - -class Adapter: - @classmethod - def session_adapter(cls, data, version: VersionNumber = VersionNumber.latest) -> Session: - return Session(**data) - - @classmethod - def charging_preference_adapter(cls, data, version: VersionNumber = VersionNumber.latest) -> Session: - return ChargingPreferences(**data) - - -def test_cpo_get_sessions_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get('/ocpi/cpo/2.2.1/sessions') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['id'] == SESSIONS[0]["id"] - - -def test_cpo_set_charging_preference_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.put(f'/ocpi/cpo/2.2.1/sessions/{SESSIONS[0]["id"]}/charging_preferences', - json=CHARGING_PREFERENCES) - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['energy_need'] == CHARGING_PREFERENCES["energy_need"] - - -def test_emsp_get_session_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/emsp/2.2.1/sessions/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{SESSIONS[0]["id"]}') - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == SESSIONS[0]["id"] - - -def test_emsp_add_session_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.put(f'/ocpi/emsp/2.2.1/sessions/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{SESSIONS[0]["id"]}', json=SESSIONS[0]) - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == SESSIONS[0]["id"] - - -def test_emsp_patch_session_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - patch_data = {'id': str(uuid4())} - client = TestClient(app) - response = client.patch(f'/ocpi/emsp/2.2.1/sessions/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{SESSIONS[0]["id"]}', json=patch_data) - - assert response.status_code == 200 - assert response.json()['data'][0]['id'] == patch_data["id"] diff --git a/tests/test_modules/test_tariffs.py b/tests/test_modules/test_tariffs.py deleted file mode 100644 index a33c783..0000000 --- a/tests/test_modules/test_tariffs.py +++ /dev/null @@ -1,101 +0,0 @@ -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.tariffs.v_2_2_1.schemas import Tariff -from py_ocpi.modules.versions.enums import VersionNumber - -TARIFFS = [{ - 'country_code': 'MY', - 'party_id': 'JOM', - 'id': str(uuid4()), - 'currency': 'MYR', - 'type': 'REGULAR', - '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 - - -class Adapter: - @classmethod - def tariff_adapter(cls, data, version: VersionNumber = VersionNumber.latest) -> Tariff: - return Tariff(**data) - - -def test_cpo_get_tariffs_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get('/ocpi/cpo/2.2.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_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/emsp/2.2.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_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.put(f'/ocpi/emsp/2.2.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_delete_tariff_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.delete(f'/ocpi/emsp/2.2.1/tariffs/{settings.COUNTRY_CODE}/{settings.PARTY_ID}' - f'/{TARIFFS[0]["id"]}') - - assert response.status_code == 200 diff --git a/tests/test_modules/test_tokens.py b/tests/test_modules/test_tokens.py deleted file mode 100644 index e87acaf..0000000 --- a/tests/test_modules/test_tokens.py +++ /dev/null @@ -1,173 +0,0 @@ -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.exceptions import NotFoundOCPIError -from py_ocpi.modules.cdrs.v_2_2_1.enums import AuthMethod -from py_ocpi.modules.tokens.v_2_2_1.enums import WhitelistType, TokenType, AllowedType -from py_ocpi.modules.tokens.v_2_2_1.schemas import AuthorizationInfo, Token -from py_ocpi.modules.versions.enums import VersionNumber - -TOKENS = [ - { - 'country_code': 'us', - 'party_id': 'AAA', - 'uid': str(uuid4()), - 'type': TokenType.rfid, - 'contract_id': str(uuid4()), - 'issuer': 'issuer', - 'auth_method': AuthMethod.auth_request, - 'valid': True, - 'whitelist': WhitelistType.always, - 'last_updated': '2022-01-02 00:00:00+00:00' - } -] - -TOKEN_UPDATE = { - 'country_code': 'pl', - 'party_id': 'BBB', - 'last_updated': '2022-01-02 00:00:00+00:00' -} - - -class Crud: - - @classmethod - async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, filters: dict, *args, **kwargs) -> Token: - return TOKENS[0] - - @classmethod - async def create(cls, module: enums.ModuleID, role: enums.RoleEnum, data: Token, *args, **kwargs) -> dict: - return data - - @classmethod - async def do(cls, module: enums.ModuleID, role: enums.RoleEnum, action: enums.Action, *args, - data: dict = None, **kwargs): - return AuthorizationInfo( - allowed=AllowedType.allowed, - token=Token(**TOKENS[0]) - ).dict() - - @classmethod - async def list(cls, module: enums.ModuleID, role: enums.RoleEnum, filters: dict, *args, **kwargs) -> list: - return TOKENS, 1, True - - @classmethod - async def update(cls, module: enums.ModuleID, role: enums.RoleEnum, data: Token, id: str, *args, **kwargs): - data = dict(data) - TOKENS[0]['country_code'] = data['country_code'] - TOKENS[0]['party_id'] = data['party_id'] - return TOKENS[0] - - -class Adapter: - @classmethod - def token_adapter(cls, data, version: VersionNumber = VersionNumber.latest) -> Token: - return Token(**dict(data)) - - @classmethod - def authorization_adapter(cls, data: dict, version: VersionNumber = VersionNumber.latest): - return AuthorizationInfo(**data) - - -def test_cpo_get_token_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get(f'/ocpi/cpo/2.2.1/tokens/{TOKENS[0]["country_code"]}/{TOKENS[0]["party_id"]}/' - f'{TOKENS[0]["uid"]}') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['uid'] == TOKENS[0]["uid"] - - -def test_cpo_add_token_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.put(f'/ocpi/cpo/2.2.1/tokens/{TOKENS[0]["country_code"]}/{TOKENS[0]["party_id"]}/' - f'{TOKENS[0]["uid"]}', json=TOKENS[0]) - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['uid'] == TOKENS[0]["uid"] - - -def test_cpo_update_token_v_2_2_1(): - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.patch(f'/ocpi/cpo/2.2.1/tokens/{TOKENS[0]["country_code"]}/{TOKENS[0]["party_id"]}/' - f'{TOKENS[0]["uid"]}', json=TOKEN_UPDATE) - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['country_code'] == TOKEN_UPDATE['country_code'] - - -def test_emsp_get_tokens_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.get('/ocpi/emsp/2.2.1/tokens') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['uid'] == TOKENS[0]["uid"] - - -def test_emsp_authorize_token_success_v_2_2_1(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.post(f'/ocpi/emsp/2.2.1/tokens/{TOKENS[0]["uid"]}/authorize') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - assert response.json()['data'][0]['allowed'] == AllowedType.allowed - - -def test_emsp_authorize_token_unknown_v_2_2_1(): - - @classmethod - async def get(cls, module: enums.ModuleID, role: enums.RoleEnum, filters: dict, *args, **kwargs) -> Token: - raise NotFoundOCPIError() - _get = Crud.get - Crud.get = get - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.post(f'/ocpi/emsp/2.2.1/tokens/{TOKENS[0]["uid"]}/authorize') - - assert response.status_code == 404 - assert response.json()['status_code'] == 2004 - - # revert Crud changes - Crud.get = _get - - -def test_emsp_authorize_token_missing_info_v_2_2_1(): - - @classmethod - async def do(cls, module: enums.ModuleID, role: enums.RoleEnum, action: enums.Action, *args, - data: dict = None, **kwargs): - return False - _do = Crud.do - Crud.do = do - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.emsp], Crud, Adapter) - - client = TestClient(app) - response = client.post(f'/ocpi/emsp/2.2.1/tokens/{TOKENS[0]["uid"]}/authorize') - - assert response.status_code == 200 - assert response.json()['status_code'] == 2002 - - # revert Crud changes - Crud.do = _do diff --git a/tests/test_modules/test_v_2_1_1/__init__.py b/tests/test_modules/test_v_2_1_1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_cdrs/__init__.py b/tests/test_modules/test_v_2_1_1/test_cdrs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_cdrs/conftest.py b/tests/test_modules/test_v_2_1_1/test_cdrs/conftest.py new file mode 100644 index 0000000..ee86c91 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_cdrs/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def cdr_cpo_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.cdrs], + ) + + +@pytest.fixture +def cdr_emsp_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.cdrs], + ) + + +@pytest.fixture +def client_cpo_v_2_1_1(cdr_cpo_v_2_1_1): + return TestClient(cdr_cpo_v_2_1_1) + + +@pytest.fixture +def client_emsp_v_2_1_1(cdr_emsp_v_2_1_1): + return TestClient(cdr_emsp_v_2_1_1) diff --git a/tests/test_modules/test_v_2_1_1/test_cdrs/test_cpo.py b/tests/test_modules/test_v_2_1_1/test_cdrs/test_cpo.py new file mode 100644 index 0000000..df6bfec --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_cdrs/test_cpo.py @@ -0,0 +1,17 @@ +from .utils import CDRS, CPO_BASE_URL, AUTH_HEADERS, WRONG_AUTH_HEADERS + +GET_CDRS_URL = CPO_BASE_URL + + +def test_cpo_cdrs_not_authenticated(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_CDRS_URL, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_cpo_get_cdrs_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_CDRS_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == CDRS[0]["id"] diff --git a/tests/test_modules/test_v_2_1_1/test_cdrs/test_emsp.py b/tests/test_modules/test_v_2_1_1/test_cdrs/test_emsp.py new file mode 100644 index 0000000..f26eab2 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_cdrs/test_emsp.py @@ -0,0 +1,42 @@ +from .utils import CDRS, AUTH_HEADERS, EMSP_BASE_URL, WRONG_AUTH_HEADERS + +GET_CDR_URL = f'{EMSP_BASE_URL}{CDRS[0]["id"]}' +POST_CDR_URL = EMSP_BASE_URL + + +def test_emsp_get_cdr_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get( + GET_CDR_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_add_cdr_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.post( + POST_CDR_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_cdr_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(GET_CDR_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == CDRS[0]["id"] + + +def test_emsp_add_cdr_v_2_1_1(client_emsp_v_2_1_1): + data = CDRS[0] + response = client_emsp_v_2_1_1.post( + POST_CDR_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == CDRS[0]["id"] + assert response.headers["Location"] is not None diff --git a/tests/test_modules/test_v_2_1_1/test_cdrs/utils.py b/tests/test_modules/test_v_2_1_1/test_cdrs/utils.py new file mode 100644 index 0000000..c799378 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_cdrs/utils.py @@ -0,0 +1,85 @@ +from uuid import uuid4 + +from py_ocpi.core import enums +from py_ocpi.modules.cdrs.v_2_1_1.enums import AuthMethod, CdrDimensionType + +from tests.test_modules.utils import ( + AUTH_TOKEN, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) +from tests.test_modules.test_v_2_1_1.test_locations.utils import LOCATIONS + + +CPO_BASE_URL = "/ocpi/cpo/2.1.1/cdrs/" +EMSP_BASE_URL = "/ocpi/emsp/2.1.1/cdrs/" +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} + +CDRS = [ + { + "id": str(uuid4()), + "start_date_time": "2022-01-02 00:00:00+00:00", + "end_date_time": "2022-01-02 00:00:00+00:00", + "auth_id": "DE8ACC12E46L89", + "auth_method": AuthMethod.auth_request, + "location": LOCATIONS[0], + "currency": "EUR", + "tariffs": [ + { + "id": "12", + "currency": "EUR", + "elements": [ + { + "price_components": [ + {"type": "TIME", "price": 2.00, "step_size": 300} + ] + } + ], + "last_updated": "2022-01-02 00:00:00+00:00", + } + ], + "charging_periods": [ + { + "start_date_time": "2022-01-02 00:00:00+00:00", + "dimensions": [ + {"type": CdrDimensionType.time, "volume": 1.973} + ], + } + ], + "total_cost": 4.00, + "total_energy": 15.342, + "total_time": 1.973, + "last_updated": "2022-01-02 00:00:00+00:00", + } +] + + +class Crud: + @classmethod + async def list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return CDRS, 1, True + + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ): + return CDRS[0] + + @classmethod + async def create( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + *args, + **kwargs, + ): + return data diff --git a/tests/test_modules/test_v_2_1_1/test_commands/__init__.py b/tests/test_modules/test_v_2_1_1/test_commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_commands/conftest.py b/tests/test_modules/test_v_2_1_1/test_commands/conftest.py new file mode 100644 index 0000000..d08aa8f --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_commands/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def command_cpo_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands, enums.ModuleID.sessions], + ) + + +@pytest.fixture +def command_emsp_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands, enums.ModuleID.sessions], + ) + + +@pytest.fixture +def client_cpo_v_2_1_1(command_cpo_v_2_1_1): + return TestClient(command_cpo_v_2_1_1) + + +@pytest.fixture +def client_emsp_v_2_1_1(command_emsp_v_2_1_1): + return TestClient(command_emsp_v_2_1_1) diff --git a/tests/test_modules/test_v_2_1_1/test_commands/test_cpo.py b/tests/test_modules/test_v_2_1_1/test_commands/test_cpo.py new file mode 100644 index 0000000..18b299b --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_commands/test_cpo.py @@ -0,0 +1,152 @@ +import pytest + +import datetime +from uuid import uuid4 + +from fastapi.testclient import TestClient + +from py_ocpi import get_application +from py_ocpi.core import enums +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.modules.commands.v_2_1_1.enums import ( + CommandType, + CommandResponseType, +) +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import ( + Crud, + ClientAuthenticator, + COMMAND_RESPONSE, + CPO_BASE_URL, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, +) + +from tests.test_modules.test_v_2_1_1.test_tokens.utils import TOKENS + +COMMAND_START_URL = f"{CPO_BASE_URL}{CommandType.start_session.value}" +COMMAND_STOP_URL = f"{CPO_BASE_URL}{CommandType.stop_session.value}" +RESERVE_NOW_URL = f"{CPO_BASE_URL}{CommandType.reserve_now.value}" + + +@pytest.mark.parametrize( + "endpoint", + [ + COMMAND_START_URL, + COMMAND_STOP_URL, + RESERVE_NOW_URL, + ], +) +def test_cpo_receive_command_start_session_not_authenticated( + client_cpo_v_2_1_1, + endpoint, +): + response = client_cpo_v_2_1_1.post( + endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_receive_command_start_session_v_2_1_1(client_cpo_v_2_1_1): + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "token": TOKENS[0], + "location_id": str(uuid4()), + } + + response = client_cpo_v_2_1_1.post( + COMMAND_START_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["result"] == COMMAND_RESPONSE["result"] + + +def test_cpo_receive_command_stop_session_v_2_1_1(client_cpo_v_2_1_1): + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "session_id": str(uuid4()), + } + + response = client_cpo_v_2_1_1.post( + COMMAND_STOP_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["result"] == COMMAND_RESPONSE["result"] + + +def test_cpo_receive_command_reserve_now_v_2_1_1(client_cpo_v_2_1_1): + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "token": TOKENS[0], + "expiry_date": str( + datetime.datetime.now() + datetime.timedelta(days=1) + ), + "reservation_id": 0, + "location_id": str(uuid4()), + } + + response = client_cpo_v_2_1_1.post( + RESERVE_NOW_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["result"] == COMMAND_RESPONSE["result"] + + +def test_cpo_receive_command_reserve_now_unknown_location_v_2_1_1(): + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ) -> dict: + if module == enums.ModuleID.commands: + return COMMAND_RESPONSE + if module == enums.ModuleID.locations: + raise NotFoundOCPIError() + + _get = Crud.get + Crud.get = get + + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands], + ) + + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "token": TOKENS[0], + "expiry_date": str( + datetime.datetime.now() + datetime.timedelta(days=1) + ), + "reservation_id": 0, + "location_id": str(uuid4()), + } + + client = TestClient(app) + response = client.post( + RESERVE_NOW_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["result"] == CommandResponseType.rejected + + # revert Crud changes + Crud.get = _get diff --git a/tests/test_modules/test_v_2_1_1/test_commands/test_emsp.py b/tests/test_modules/test_v_2_1_1/test_commands/test_emsp.py new file mode 100644 index 0000000..527eb7e --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_commands/test_emsp.py @@ -0,0 +1,28 @@ +from .utils import ( + EMSP_BASE_URL, + WRONG_AUTH_HEADERS, + AUTH_HEADERS, + COMMAND_RESPONSE, +) + +RECEIVE_URL = f"{EMSP_BASE_URL}1234" + + +def test_emsp_receive_command_result_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.post( + RECEIVE_URL, + json=COMMAND_RESPONSE, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_receive_command_result_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.post( + RECEIVE_URL, + json=COMMAND_RESPONSE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 diff --git a/tests/test_modules/test_v_2_1_1/test_commands/utils.py b/tests/test_modules/test_v_2_1_1/test_commands/utils.py new file mode 100644 index 0000000..e03b6f9 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_commands/utils.py @@ -0,0 +1,53 @@ +from py_ocpi.core import enums +from py_ocpi.modules.commands.v_2_2_1.enums import ( + CommandResponseType, + CommandResultType, +) + +from tests.test_modules.utils import ( + AUTH_TOKEN, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.1.1/commands/" +EMSP_BASE_URL = "/ocpi/emsp/2.1.1/commands/" +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} + +COMMAND_RESPONSE = {"result": CommandResponseType.accepted} + + +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 + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ) -> dict: + return COMMAND_RESPONSE + + @classmethod + async def update( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + id, + *args, + **kwargs, + ): + ... diff --git a/tests/test_modules/test_v_2_1_1/test_credentials/__init__.py b/tests/test_modules/test_v_2_1_1/test_credentials/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_credentials/test_credentials.py b/tests/test_modules/test_v_2_1_1/test_credentials/test_credentials.py new file mode 100644 index 0000000..f6c2596 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_credentials/test_credentials.py @@ -0,0 +1,95 @@ +from uuid import uuid4 +from unittest.mock import patch +from typing import Any + +import pytest +from fastapi.testclient import TestClient +from httpx import AsyncClient + +from py_ocpi import get_application +from py_ocpi.core import enums +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.modules.versions.enums import VersionNumber +from py_ocpi.modules.versions.schemas import Version + +from .utils import Crud, CREDENTIALS_TOKEN_CREATE, AUTH_HEADERS, AUTH_HEADERS_A +from tests.test_modules.utils import ClientAuthenticator + + +def test_cpo_get_credentials_v_2_1_1(): + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + + client = TestClient(app) + response = client.get( + "/ocpi/cpo/2.1.1/credentials", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"]["token"] == CREDENTIALS_TOKEN_CREATE["token"] + + +@pytest.mark.asyncio +@patch("py_ocpi.modules.credentials.v_2_1_1.api.cpo.httpx.AsyncClient") +async def test_cpo_post_credentials_v_2_1_1(async_client): + 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( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + + def override_get_versions(): + return [ + Version( + version=VersionNumber.v_2_1_1, + url=URL( + f"/{settings.OCPI_PREFIX}/{VersionNumber.v_2_1_1.value}/details" + ), + ).dict() + ] + + app_1.dependency_overrides[get_versions] = override_get_versions + + async_client.return_value = AsyncClient(app=app_1, base_url="http://test") + + app_2 = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + + async with AsyncClient(app=app_2, base_url="http://test") as client: + response = await client.post( + "/ocpi/cpo/2.1.1/credentials/", + json=CREDENTIALS_TOKEN_CREATE, + headers=AUTH_HEADERS_A, + ) + + assert response.status_code == 200 + assert response.json()["data"]["token"] == CREDENTIALS_TOKEN_CREATE["token"] diff --git a/tests/test_modules/test_v_2_1_1/test_credentials/utils.py b/tests/test_modules/test_v_2_1_1/test_credentials/utils.py new file mode 100644 index 0000000..eefa7d4 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_credentials/utils.py @@ -0,0 +1,70 @@ +import functools +from uuid import uuid4 + +from py_ocpi.core import enums + +from tests.test_modules.utils import ( + AUTH_TOKEN, + AUTH_TOKEN_A, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +AUTH_HEADERS_A = {"Authorization": f"Token {AUTH_TOKEN_A}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} + + +CREDENTIALS_TOKEN_GET = { + "url": "url", + "business_details": { + "name": "name", + }, + "party_id": "JOM", + "country_code": "MY", +} + +CREDENTIALS_TOKEN_CREATE = { + "token": AUTH_TOKEN_A, + "url": "/ocpi/versions", + "business_details": { + "name": "name", + }, + "party_id": "JOM", + "country_code": "MY", +} + + +def partial_class(cls, *args, **kwds): + class NewCls(cls): + __init__ = functools.partialmethod(cls.__init__, *args, **kwds) + + return NewCls + + +class Crud: + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ): + return CREDENTIALS_TOKEN_CREATE + + @classmethod + async def create( + cls, module: enums.ModuleID, data, operation, *args, **kwargs + ): + if operation == "credentials": + return None + return CREDENTIALS_TOKEN_CREATE + + @classmethod + async def do( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + action: enums.Action, + *args, + data: dict = None, + **kwargs, + ): + return None diff --git a/tests/test_modules/test_v_2_1_1/test_locations/__init__.py b/tests/test_modules/test_v_2_1_1/test_locations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_locations/conftest.py b/tests/test_modules/test_v_2_1_1/test_locations/conftest.py new file mode 100644 index 0000000..7c52d6a --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_locations/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def location_cpo_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) + + +@pytest.fixture +def location_emsp_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) + + +@pytest.fixture +def client_cpo_v_2_1_1(location_cpo_v_2_1_1): + return TestClient(location_cpo_v_2_1_1) + + +@pytest.fixture +def client_emsp_v_2_1_1(location_emsp_v_2_1_1): + return TestClient(location_emsp_v_2_1_1) diff --git a/tests/test_modules/test_v_2_1_1/test_locations/test_cpo.py b/tests/test_modules/test_v_2_1_1/test_locations/test_cpo.py new file mode 100644 index 0000000..0244198 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_locations/test_cpo.py @@ -0,0 +1,63 @@ +import pytest + +from .utils import CPO_BASE_URL, AUTH_HEADERS, LOCATIONS, WRONG_AUTH_HEADERS + +GET_LOCATIONS_URL = CPO_BASE_URL +GET_LOCATION_URL = f'{CPO_BASE_URL}{LOCATIONS[0]["id"]}' +GET_EVSE_URL = ( + f'{CPO_BASE_URL}{LOCATIONS[0]["id"]}' f'/{LOCATIONS[0]["evses"][0]["uid"]}' +) +GET_CONNECTOR_URL = ( + f'{CPO_BASE_URL}{LOCATIONS[0]["id"]}' + f'/{LOCATIONS[0]["evses"][0]["uid"]}' + f'/{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}' +) + + +@pytest.mark.parametrize( + "endpoint", + [ + GET_LOCATIONS_URL, + GET_LOCATION_URL, + GET_EVSE_URL, + GET_CONNECTOR_URL, + ], +) +def test_cpo_locations_not_authenticated(client_cpo_v_2_1_1, endpoint): + response = client_cpo_v_2_1_1.get(endpoint, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_cpo_get_locations_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_LOCATIONS_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_cpo_get_location_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_LOCATION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_cpo_get_evse_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_EVSE_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == LOCATIONS[0]["evses"][0]["uid"] + + +def test_cpo_get_connector_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_CONNECTOR_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["id"] + == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] + ) diff --git a/tests/test_modules/test_v_2_1_1/test_locations/test_emsp.py b/tests/test_modules/test_v_2_1_1/test_locations/test_emsp.py new file mode 100644 index 0000000..e431641 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_locations/test_emsp.py @@ -0,0 +1,174 @@ +import pytest + +from uuid import uuid4 + +from py_ocpi.core.config import settings + +from .utils import EMSP_BASE_URL, AUTH_HEADERS, LOCATIONS, WRONG_AUTH_HEADERS + +LOCATION_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f"{LOCATIONS[0]['id']}" +) +EVSE_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f"{LOCATIONS[0]['id']}/{LOCATIONS[0]['evses'][0]['uid']}" +) +CONNECTOR_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f'{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}/' + f'{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}' +) + + +@pytest.mark.parametrize( + "endpoint", + [ + LOCATION_URL, + EVSE_URL, + CONNECTOR_URL, + ], +) +def test_emsp_get_locations_not_authenticated(client_emsp_v_2_1_1, endpoint): + response = client_emsp_v_2_1_1.get( + url=endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +@pytest.mark.parametrize( + "endpoint", + [ + LOCATION_URL, + EVSE_URL, + CONNECTOR_URL, + ], +) +def test_emsp_put_locations_not_authenticated(client_emsp_v_2_1_1, endpoint): + response = client_emsp_v_2_1_1.put( + url=endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +@pytest.mark.parametrize( + "endpoint", + [ + LOCATION_URL, + EVSE_URL, + CONNECTOR_URL, + ], +) +def test_emsp_patch_locations_not_authenticated(client_emsp_v_2_1_1, endpoint): + response = client_emsp_v_2_1_1.patch( + url=endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_location_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(LOCATION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_emsp_get_evse_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(EVSE_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == LOCATIONS[0]["evses"][0]["uid"] + + +def test_emsp_get_connector_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(CONNECTOR_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["id"] + == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] + ) + + +def test_emsp_add_location_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.put( + LOCATION_URL, + json=LOCATIONS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_emsp_add_evse_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.put( + EVSE_URL, + json=LOCATIONS[0]["evses"][0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == LOCATIONS[0]["evses"][0]["uid"] + + +def test_emsp_add_connector_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.put( + CONNECTOR_URL, + json=LOCATIONS[0]["evses"][0]["connectors"][0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["id"] + == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] + ) + + +def test_emsp_patch_location_v_2_1_1(client_emsp_v_2_1_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_1_1.patch( + LOCATION_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == patch_data["id"] + + +def test_emsp_patch_evse_v_2_1_1(client_emsp_v_2_1_1): + patch_data = {"uid": str(uuid4())} + response = client_emsp_v_2_1_1.patch( + EVSE_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == patch_data["uid"] + + +def test_emsp_patch_connector_v_2_1_1(client_emsp_v_2_1_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_1_1.patch( + CONNECTOR_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == patch_data["id"] diff --git a/tests/test_modules/test_v_2_1_1/test_locations/utils.py b/tests/test_modules/test_v_2_1_1/test_locations/utils.py new file mode 100644 index 0000000..21d4614 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_locations/utils.py @@ -0,0 +1,214 @@ +from uuid import uuid4 + +from py_ocpi.core import enums + +from tests.test_modules.utils import ( + AUTH_TOKEN, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + + +CPO_BASE_URL = "/ocpi/cpo/2.1.1/locations/" +EMSP_BASE_URL = "/ocpi/emsp/2.1.1/locations/" +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} + +LOCATIONS = [ + { + "id": str(uuid4()), + "type": "ON_STREET", + "name": "name", + "address": "address", + "city": "city", + "postal_code": "111111", + "country": "USA", + "coordinates": { + "latitude": "latitude", + "longitude": "longitude", + }, + "related_locations": [ + { + "latitude": "latitude", + "longitude": "longitude", + "name": {"language": "en", "text": "name"}, + }, + ], + "evses": [ + { + "uid": str(uuid4()), + "evse_id": str(uuid4()), + "status": "AVAILABLE", + "status_schedule": { + "period_begin": "2022-01-01T00:00:00+00:00", + "period_end": "2022-01-01T00:00:00+00:00", + "status": "AVAILABLE", + }, + "capabilities": [ + "CREDIT_CARD_PAYABLE", + ], + "connectors": [ + { + "id": str(uuid4()), + "standard": "DOMESTIC_A", + "format": "SOCKET", + "power_type": "DC", + "voltage": 100, + "amperage": 100, + "tariff_id": str(uuid4()), + "terms_and_conditions": "https://www.example.com", + "last_updated": "2022-01-01T00:00:00+00:00", + } + ], + "floor_level": "3", + "coordinates": { + "latitude": "latitude", + "longitude": "longitude", + }, + "physical_reference": "pr", + "directions": [ + {"language": "en", "text": "directions"}, + ], + "parking_restrictions": [ + "EV_ONLY", + ], + "images": [ + { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + ], + "last_updated": "2022-01-01T00:00:00+00:00", + } + ], + "directions": [ + {"language": "en", "text": "directions"}, + ], + "operator": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + }, + "suboperator": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + }, + "owner": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + }, + "facilities": ["MALL"], + "time_zone": "UTC+2", + "opening_times": { + "twentyfourseven": True, + "regular_hours": [ + { + "weekday": 1, + "period_begin": "8:00", + "period_end": "22:00", + }, + { + "weekday": 2, + "period_begin": "8:00", + "period_end": "22:00", + }, + ], + "exceptional_openings": [ + { + "period_begin": "2022-01-01T00:00:00+00:00", + "period_end": "2022-01-02T00:00:00+00:00", + }, + ], + "exceptional_closings": [], + }, + "charging_when_closed": False, + "images": [ + { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + ], + "energy_mix": { + "is_green_energy": True, + "energy_sources": [ + {"source": "SOLAR", "percentage": 100}, + ], + "supplier_name": "supplier_name", + "energy_product_name": "energy_product_name", + }, + "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 LOCATIONS[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 list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return LOCATIONS, 1, True diff --git a/tests/test_modules/test_v_2_1_1/test_sessions/__init__.py b/tests/test_modules/test_v_2_1_1/test_sessions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_sessions/conftest.py b/tests/test_modules/test_v_2_1_1/test_sessions/conftest.py new file mode 100644 index 0000000..85d5441 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_sessions/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def session_cpo_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.sessions], + ) + + +@pytest.fixture +def session_emsp_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.sessions], + ) + + +@pytest.fixture +def client_cpo_v_2_1_1(session_cpo_v_2_1_1): + return TestClient(session_cpo_v_2_1_1) + + +@pytest.fixture +def client_emsp_v_2_1_1(session_emsp_v_2_1_1): + return TestClient(session_emsp_v_2_1_1) diff --git a/tests/test_modules/test_v_2_1_1/test_sessions/test_cpo.py b/tests/test_modules/test_v_2_1_1/test_sessions/test_cpo.py new file mode 100644 index 0000000..e0a0d7c --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_sessions/test_cpo.py @@ -0,0 +1,20 @@ +from .utils import CPO_BASE_URL, SESSIONS, AUTH_HEADERS, WRONG_AUTH_HEADERS + +GET_SESSIONS_URL = CPO_BASE_URL + + +def test_cpo_get_sessions_not_authenticated(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get( + GET_SESSIONS_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_sessions_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_SESSIONS_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == SESSIONS[0]["id"] diff --git a/tests/test_modules/test_v_2_1_1/test_sessions/test_emsp.py b/tests/test_modules/test_v_2_1_1/test_sessions/test_emsp.py new file mode 100644 index 0000000..a72263c --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_sessions/test_emsp.py @@ -0,0 +1,67 @@ +from uuid import uuid4 + +from py_ocpi.core.config import settings + +from .utils import EMSP_BASE_URL, SESSIONS, AUTH_HEADERS, WRONG_AUTH_HEADERS + +GET_SESSION = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f'{SESSIONS[0]["id"]}' +) + + +def test_emsp_get_session_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(GET_SESSION, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_emsp_add_session_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.put( + GET_SESSION, + json=SESSIONS[0], + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_patch_session_not_authenticated(client_emsp_v_2_1_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_1_1.patch( + GET_SESSION, + json=patch_data, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_session_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(GET_SESSION, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == SESSIONS[0]["id"] + + +def test_emsp_add_session_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.put( + GET_SESSION, + json=SESSIONS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == SESSIONS[0]["id"] + + +def test_emsp_patch_session_v_2_1_1(client_emsp_v_2_1_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_1_1.patch( + GET_SESSION, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == patch_data["id"] diff --git a/tests/test_modules/test_v_2_1_1/test_sessions/utils.py b/tests/test_modules/test_v_2_1_1/test_sessions/utils.py new file mode 100644 index 0000000..c355452 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_sessions/utils.py @@ -0,0 +1,85 @@ +from uuid import uuid4 + +from py_ocpi.core import enums +from py_ocpi.modules.cdrs.v_2_1_1.enums import AuthMethod, CdrDimensionType +from py_ocpi.modules.sessions.v_2_1_1.enums import SessionStatus + +from tests.test_modules.utils import ( + AUTH_TOKEN, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) +from tests.test_modules.test_v_2_1_1.test_locations.utils import LOCATIONS + +CPO_BASE_URL = "/ocpi/cpo/2.1.1/sessions/" +EMSP_BASE_URL = "/ocpi/emsp/2.1.1/sessions/" +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} + +SESSIONS = [ + { + "id": str(uuid4()), + "start_datetime": "2022-01-02 00:00:00+00:00", + "end_datetime": "2022-01-02 00:05:00+00:00", + "kwh": 100, + "auth_id": "100", + "auth_method": AuthMethod.auth_request, + "location": LOCATIONS[0], + "currency": "MYR", + "charging_periods": [ + { + "start_date_time": "2022-01-02 00:00:00+00:00", + "dimensions": [ + { + "type": CdrDimensionType.time, + "volume": 10, + } + ], + } + ], + "status": SessionStatus.active, + "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 SESSIONS[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 list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return SESSIONS, 1, True diff --git a/tests/test_modules/test_v_2_1_1/test_tariffs/__init__.py b/tests/test_modules/test_v_2_1_1/test_tariffs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_tariffs/conftest.py b/tests/test_modules/test_v_2_1_1/test_tariffs/conftest.py new file mode 100644 index 0000000..18aaca5 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tariffs/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def tariff_cpo_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tariffs], + ) + + +@pytest.fixture +def tariff_emsp_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tariffs], + ) + + +@pytest.fixture +def client_cpo_v_2_1_1(tariff_cpo_v_2_1_1): + return TestClient(tariff_cpo_v_2_1_1) + + +@pytest.fixture +def client_emsp_v_2_1_1(tariff_emsp_v_2_1_1): + return TestClient(tariff_emsp_v_2_1_1) diff --git a/tests/test_modules/test_v_2_1_1/test_tariffs/test_cpo.py b/tests/test_modules/test_v_2_1_1/test_tariffs/test_cpo.py new file mode 100644 index 0000000..b4dc880 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tariffs/test_cpo.py @@ -0,0 +1,20 @@ +from .utils import TARIFFS, CPO_BASE_URL, AUTH_HEADERS, WRONG_AUTH_HEADERS + +GET_TARIFFS_URL = CPO_BASE_URL + + +def test_cpo_get_tariffs_not_authenticated(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get( + GET_TARIFFS_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_tariffs_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(GET_TARIFFS_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] diff --git a/tests/test_modules/test_v_2_1_1/test_tariffs/test_emsp.py b/tests/test_modules/test_v_2_1_1/test_tariffs/test_emsp.py new file mode 100644 index 0000000..b020ddf --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tariffs/test_emsp.py @@ -0,0 +1,81 @@ +from uuid import uuid4 + +from py_ocpi.core.config import settings + +from .utils import EMSP_BASE_URL, TARIFFS, AUTH_HEADERS, WRONG_AUTH_HEADERS + + +TARIFF_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f'{TARIFFS[0]["id"]}' +) + + +def test_emsp_get_tariff_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(TARIFF_URL, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_emsp_add_tariff_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.put( + TARIFF_URL, + json=[0], + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_patch_tariff_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.patch( + TARIFF_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_delete_tariff_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.delete( + TARIFF_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_tariff_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(TARIFF_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] + + +def test_emsp_add_tariff_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.put( + TARIFF_URL, + json=TARIFFS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] + + +def test_emsp_patch_tariff_v_2_1_1(client_emsp_v_2_1_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_1_1.patch( + TARIFF_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == patch_data["id"] + + +def test_emsp_delete_tariff_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.delete(TARIFF_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 diff --git a/tests/test_modules/test_v_2_1_1/test_tariffs/utils.py b/tests/test_modules/test_v_2_1_1/test_tariffs/utils.py new file mode 100644 index 0000000..063d912 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tariffs/utils.py @@ -0,0 +1,81 @@ +from uuid import uuid4 + +from py_ocpi.core import enums + +from tests.test_modules.utils import ( + AUTH_TOKEN, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.1.1/tariffs/" +EMSP_BASE_URL = "/ocpi/emsp/2.1.1/tariffs/" +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} + +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 diff --git a/tests/test_modules/test_v_2_1_1/test_tokens/__init__.py b/tests/test_modules/test_v_2_1_1/test_tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_tokens/conftest.py b/tests/test_modules/test_v_2_1_1/test_tokens/conftest.py new file mode 100644 index 0000000..bbb8b8b --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tokens/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def tokens_cpo_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tokens], + ) + + +@pytest.fixture +def tokens_emsp_v_2_1_1(): + return get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tokens], + ) + + +@pytest.fixture +def client_cpo_v_2_1_1(tokens_cpo_v_2_1_1): + return TestClient(tokens_cpo_v_2_1_1) + + +@pytest.fixture +def client_emsp_v_2_1_1(tokens_emsp_v_2_1_1): + return TestClient(tokens_emsp_v_2_1_1) diff --git a/tests/test_modules/test_v_2_1_1/test_tokens/test_cpo.py b/tests/test_modules/test_v_2_1_1/test_tokens/test_cpo.py new file mode 100644 index 0000000..fb573cc --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tokens/test_cpo.py @@ -0,0 +1,70 @@ +from py_ocpi.core.config import settings + +from .utils import ( + TOKENS, + TOKEN_UPDATE, + CPO_BASE_URL, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, +) + +TOKEN_URL = ( + f"{CPO_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f'{TOKENS[0]["uid"]}' +) + + +def test_cpo_get_token_not_authenticated(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(TOKEN_URL, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_cpo_add_token_not_authenticate(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.put( + TOKEN_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_update_token_not_authenticate(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.patch( + TOKEN_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_token_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.get(TOKEN_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == TOKENS[0]["uid"] + + +def test_cpo_add_token_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.put( + TOKEN_URL, + json=TOKENS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == TOKENS[0]["uid"] + + +def test_cpo_update_token_v_2_1_1(client_cpo_v_2_1_1): + response = client_cpo_v_2_1_1.patch( + TOKEN_URL, + json=TOKEN_UPDATE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["valid"] == TOKEN_UPDATE["valid"] diff --git a/tests/test_modules/test_v_2_1_1/test_tokens/test_emsp.py b/tests/test_modules/test_v_2_1_1/test_tokens/test_emsp.py new file mode 100644 index 0000000..004d402 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tokens/test_emsp.py @@ -0,0 +1,39 @@ +from py_ocpi.modules.tokens.v_2_1_1.enums import Allowed + +from .utils import ( + EMSP_BASE_URL, + TOKENS, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, +) + +GET_TOKEN = EMSP_BASE_URL +POST_TOKEN = f'{EMSP_BASE_URL}{TOKENS[0]["uid"]}/authorize' + + +def test_emsp_get_tokens_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(GET_TOKEN, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_emsp_authorize_token_not_authenticated(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.post(POST_TOKEN, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_emsp_get_tokens_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.get(GET_TOKEN, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == TOKENS[0]["uid"] + + +def test_emsp_authorize_token_success_v_2_1_1(client_emsp_v_2_1_1): + response = client_emsp_v_2_1_1.post(POST_TOKEN, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["allowed"] == Allowed.allowed diff --git a/tests/test_modules/test_v_2_1_1/test_tokens/utils.py b/tests/test_modules/test_v_2_1_1/test_tokens/utils.py new file mode 100644 index 0000000..e53a927 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_tokens/utils.py @@ -0,0 +1,100 @@ +from uuid import uuid4 + +from py_ocpi.core import enums + +from py_ocpi.modules.tokens.v_2_1_1.enums import ( + WhitelistType, + TokenType, + Allowed, +) +from py_ocpi.modules.tokens.v_2_1_1.schemas import AuthorizationInfo, Token + +from tests.test_modules.utils import ( + AUTH_TOKEN, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.1.1/tokens/" +EMSP_BASE_URL = "/ocpi/emsp/2.1.1/tokens/" +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} + +TOKENS = [ + { + "uid": str(uuid4()), + "type": TokenType.rfid, + "auth_id": str(uuid4()), + "issuer": "issuer", + "valid": True, + "whitelist": WhitelistType.always, + "last_updated": "2022-01-02 00:00:00+00:00", + } +] + +TOKEN_UPDATE = { + "valid": False, +} + + +class Crud: + @classmethod + async def get( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> Token: + return TOKENS[0] + + @classmethod + async def create( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: Token, + *args, + **kwargs, + ) -> dict: + return data + + @classmethod + async def do( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + action: enums.Action, + *args, + data: dict = None, + **kwargs, + ): + return AuthorizationInfo( + allowed=Allowed.allowed, token=Token(**TOKENS[0]) + ).dict() + + @classmethod + async def list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return TOKENS, 1, True + + @classmethod + async def update( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: Token, + id: str, + *args, + **kwargs, + ): + data = dict(data) + TOKENS[0]["valid"] = data["valid"] + return TOKENS[0] diff --git a/tests/test_modules/test_v_2_1_1/test_versions/__init__.py b/tests/test_modules/test_v_2_1_1/test_versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_versions/conftest.py b/tests/test_modules/test_v_2_1_1/test_versions/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_1_1/test_versions/test_versions.py b/tests/test_modules/test_v_2_1_1/test_versions/test_versions.py new file mode 100644 index 0000000..f0d6f84 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_versions/test_versions.py @@ -0,0 +1,106 @@ +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.core.crud import Crud +from py_ocpi.modules.versions.enums import VersionNumber + +from tests.test_modules.utils import AUTH_TOKEN, ClientAuthenticator +from .utils import AUTH_HEADERS, WRONG_AUTH_HEADERS + +VERSIONS_URL = "/ocpi/versions" +VERSION_URL = "/ocpi/2.1.1/details" + + +def test_get_versions(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSIONS_URL, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + + +def test_get_versions_not_authenticated(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return None + + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSIONS_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 401 + + +def test_get_versions_v_2_1_1(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSION_URL, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 2 + + +def test_get_versions_v_2_1_1_not_authenticated(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return None + + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSION_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 401 diff --git a/tests/test_modules/test_v_2_1_1/test_versions/utils.py b/tests/test_modules/test_v_2_1_1/test_versions/utils.py new file mode 100644 index 0000000..f5f8cc2 --- /dev/null +++ b/tests/test_modules/test_v_2_1_1/test_versions/utils.py @@ -0,0 +1,8 @@ +from tests.test_modules.utils import ( + AUTH_TOKEN, + RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +AUTH_HEADERS = {"Authorization": f"Token {AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {RANDOM_AUTH_TOKEN}"} diff --git a/tests/test_modules/test_v_2_2_1/__init__.py b/tests/test_modules/test_v_2_2_1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_cdrs/__init__.py b/tests/test_modules/test_v_2_2_1/test_cdrs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_cdrs/conftest.py b/tests/test_modules/test_v_2_2_1/test_cdrs/conftest.py new file mode 100644 index 0000000..a9f4dff --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_cdrs/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def cdr_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.cdrs], + ) + + +@pytest.fixture +def cdr_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.cdrs], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(cdr_cpo_v_2_2_1): + return TestClient(cdr_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(cdr_emsp_v_2_2_1): + return TestClient(cdr_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_cdrs/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_cdrs/test_cpo.py new file mode 100644 index 0000000..6ef0368 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_cdrs/test_cpo.py @@ -0,0 +1,17 @@ +from .utils import CDRS, CPO_BASE_URL, AUTH_HEADERS, WRONG_AUTH_HEADERS + +GET_CDRS_URL = CPO_BASE_URL + + +def test_cpo_cdrs_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_CDRS_URL, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_cpo_get_cdrs_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_CDRS_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == CDRS[0]["id"] diff --git a/tests/test_modules/test_v_2_2_1/test_cdrs/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_cdrs/test_emsp.py new file mode 100644 index 0000000..63a96cd --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_cdrs/test_emsp.py @@ -0,0 +1,42 @@ +from .utils import CDRS, AUTH_HEADERS, EMSP_BASE_URL, WRONG_AUTH_HEADERS + +GET_CDR_URL = f'{EMSP_BASE_URL}{CDRS[0]["id"]}' +POST_CDR_URL = EMSP_BASE_URL + + +def test_emsp_get_cdr_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get( + GET_CDR_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_add_cdr_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.post( + POST_CDR_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_cdr_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(GET_CDR_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == CDRS[0]["id"] + + +def test_emsp_add_cdr_v_2_2_1(client_emsp_v_2_2_1): + data = CDRS[0] + response = client_emsp_v_2_2_1.post( + POST_CDR_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == CDRS[0]["id"] + assert response.headers["Location"] is not None diff --git a/tests/test_modules/test_v_2_2_1/test_cdrs/utils.py b/tests/test_modules/test_v_2_2_1/test_cdrs/utils.py new file mode 100644 index 0000000..7b2ecc8 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_cdrs/utils.py @@ -0,0 +1,98 @@ +from uuid import uuid4 + +from py_ocpi.core import enums +from py_ocpi.modules.locations.v_2_2_1.schemas import ( + ConnectorType, + ConnectorFormat, + PowerType, +) +from py_ocpi.modules.cdrs.v_2_2_1.schemas import TokenType +from py_ocpi.modules.cdrs.v_2_2_1.enums import AuthMethod, CdrDimensionType + +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/cdrs/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/cdrs/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + +CDRS = [ + { + "country_code": "us", + "party_id": "AAA", + "id": str(uuid4()), + "start_date_time": "2022-01-02 00:00:00+00:00", + "end_date_time": "2022-01-02 00:05:00+00:00", + "cdr_token": { + "country_code": "us", + "party_id": "AAA", + "uid": str(uuid4()), + "type": TokenType.rfid, + "contract_id": str(uuid4()), + }, + "auth_method": AuthMethod.auth_request, + "cdr_location": { + "id": str(uuid4()), + "name": "name", + "address": "address", + "city": "city", + "postal_code": "111111", + "state": "state", + "country": "USA", + "coordinates": { + "latitude": "latitude", + "longitude": "longitude", + }, + "evse_id": str(uuid4()), + "connector_id": str(uuid4()), + "connector_standard": ConnectorType.tesla_r, + "connector_format": ConnectorFormat.cable, + "connector_power_type": PowerType.dc, + }, + "currency": "MYR", + "charging_periods": [ + { + "start_date_time": "2022-01-02 00:00:00+00:00", + "dimensions": [{"type": CdrDimensionType.power, "volume": 10}], + } + ], + "total_cost": {"excl_vat": 10.0000, "incl_vat": 10.2500}, + "total_energy": 50, + "total_time": 500, + "last_updated": "2022-01-02 00:00:00+00:00", + } +] + + +class Crud: + @classmethod + async def list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return CDRS, 1, True + + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ): + return CDRS[0] + + @classmethod + async def create( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + *args, + **kwargs, + ): + return data diff --git a/tests/test_modules/test_v_2_2_1/test_chargingprofiles/__init__.py b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_chargingprofiles/conftest.py b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/conftest.py new file mode 100644 index 0000000..d7de46e --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/conftest.py @@ -0,0 +1,42 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from tests.test_modules.utils import ClientAuthenticator +from .utils import Crud + + +@pytest.fixture +def chargingprofile_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.charging_profile, enums.ModuleID.sessions], + ) + + +@pytest.fixture +def chargingprofile_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.charging_profile, enums.ModuleID.sessions], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(chargingprofile_cpo_v_2_2_1): + return TestClient(chargingprofile_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(chargingprofile_emsp_v_2_2_1): + return TestClient(chargingprofile_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_cpo.py new file mode 100644 index 0000000..921050f --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_cpo.py @@ -0,0 +1,205 @@ +from unittest.mock import patch + +from py_ocpi.modules.chargingprofiles.v_2_2_1.schemas import ( + ChargingProfileResponseType, +) + +from .utils import ( + CPO_BASE_URL, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, + SET_CHARGING_PROFILE, +) + +CHARGINGPROFILE_URL = f"{CPO_BASE_URL}1234" + + +def test_cpo_get_chargingprofile_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get( + CHARGINGPROFILE_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_add_or_update_chargingprofile_not_authenticated( + client_cpo_v_2_2_1, +): + response = client_cpo_v_2_2_1.put( + CHARGINGPROFILE_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_delete_chargingprofile_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.delete( + CHARGINGPROFILE_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +@patch("tests.test_modules.test_v_2_2_1.test_chargingprofiles.utils.Crud.get") +def test_cpo_get_chargingprofile_no_session(mock_get, client_cpo_v_2_2_1): + mock_get.return_value = None + + response = client_cpo_v_2_2_1.get( + f"{CHARGINGPROFILE_URL}?duration={1}&response_url=abs", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.rejected + ) + + +@patch("tests.test_modules.test_v_2_2_1.test_chargingprofiles.utils.Crud.do") +def test_cpo_get_chargingprofile_no_charging_response( + mock_do, client_cpo_v_2_2_1 +): + mock_do.return_value = None + + response = client_cpo_v_2_2_1.get( + f"{CHARGINGPROFILE_URL}?duration={1}&response_url=abs", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.rejected + ) + + +@patch( + "py_ocpi.modules.chargingprofiles.v_2_2_1.api.cpo.BackgroundTasks.add_task" +) +def test_cpo_get_chargingprofile_v_2_2_1(mock_background, client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get( + f"{CHARGINGPROFILE_URL}?duration={1}&response_url=abs", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.accepted + ) + assert mock_background.call_count == 1 + + +@patch("tests.test_modules.test_v_2_2_1.test_chargingprofiles.utils.Crud.get") +def test_cpo_add_or_update_chargingprofile_no_session( + mock_get, client_cpo_v_2_2_1 +): + mock_get.return_value = None + + response = client_cpo_v_2_2_1.put( + CHARGINGPROFILE_URL, + json=SET_CHARGING_PROFILE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.rejected + ) + + +@patch("tests.test_modules.test_v_2_2_1.test_chargingprofiles.utils.Crud.do") +def test_cpo_add_or_update_chargingprofile_no_charging_response( + mock_do, client_cpo_v_2_2_1 +): + mock_do.return_value = None + + response = client_cpo_v_2_2_1.put( + CHARGINGPROFILE_URL, + json=SET_CHARGING_PROFILE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.rejected + ) + + +@patch( + "py_ocpi.modules.chargingprofiles.v_2_2_1.api.cpo.BackgroundTasks.add_task" +) +def test_cpo_add_or_update_chargingprofile_v_2_2_1( + mock_background, client_cpo_v_2_2_1 +): + response = client_cpo_v_2_2_1.put( + CHARGINGPROFILE_URL, + json=SET_CHARGING_PROFILE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.accepted + ) + assert mock_background.call_count == 1 + + +@patch("tests.test_modules.test_v_2_2_1.test_chargingprofiles.utils.Crud.get") +def test_cpo_delete_chargingprofile_no_session(mock_get, client_cpo_v_2_2_1): + mock_get.return_value = None + + response = client_cpo_v_2_2_1.delete( + f"{CHARGINGPROFILE_URL}?response_url=abs", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.rejected + ) + + +@patch("tests.test_modules.test_v_2_2_1.test_chargingprofiles.utils.Crud.do") +def test_cpo_delete_chargingprofile_no_charging_response( + mock_do, client_cpo_v_2_2_1 +): + mock_do.return_value = None + + response = client_cpo_v_2_2_1.delete( + f"{CHARGINGPROFILE_URL}?response_url=abs", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.rejected + ) + + +@patch( + "py_ocpi.modules.chargingprofiles.v_2_2_1.api.cpo.BackgroundTasks.add_task" +) +def test_cpo_delete_chargingprofile_v_2_2_1( + mock_background, client_cpo_v_2_2_1 +): + response = client_cpo_v_2_2_1.delete( + f"{CHARGINGPROFILE_URL}?response_url=abs", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert ( + response.json()["data"][0]["result"] + == ChargingProfileResponseType.accepted + ) + assert mock_background.call_count == 1 diff --git a/tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_emsp.py new file mode 100644 index 0000000..caa0669 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/test_emsp.py @@ -0,0 +1,49 @@ +from .utils import ( + CHARGING_PROFILE, + EMSP_BASE_URL, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, +) + +RECEIVE_URL = f"{EMSP_BASE_URL}" +ADD_OR_UPDATE_URL = f"{EMSP_BASE_URL}1234" + + +def test_emsp_receive_chargingprofile_result_not_authenticated( + client_emsp_v_2_2_1, +): + response = client_emsp_v_2_2_1.post( + RECEIVE_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_add_or_update_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + ADD_OR_UPDATE_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_receive_chargingprofile_result_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.post( + RECEIVE_URL, + json={}, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + + +def test_emsp_add_or_update_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + ADD_OR_UPDATE_URL, + json=CHARGING_PROFILE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 diff --git a/tests/test_modules/test_v_2_2_1/test_chargingprofiles/utils.py b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/utils.py new file mode 100644 index 0000000..99ecaf5 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_chargingprofiles/utils.py @@ -0,0 +1,83 @@ +from py_ocpi.core import enums + +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, +) +from tests.test_modules.test_v_2_2_1.test_sessions.utils import SESSIONS + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/chargingprofiles/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/chargingprofiles/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + +CHARGING_PROFILE = { + "start_date_time": "2022-01-02 00:00:00+00:00", + "charging_profile": { + "charging_rate_unit": "W", + "min_charge_rate": 1, + "charging_profile_period": { + "start_period": 1, + "limit": 1, + }, + }, +} + +SET_CHARGING_PROFILE = { + "charging_profile": { + "charging_rate_unit": "W", + "min_charge_rate": 1, + "charging_profile_period": { + "start_period": 1, + "limit": 1, + }, + }, + "response_url": "abc", +} + + +class Crud: + @classmethod + async def do( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + action: enums.Action, + *args, + data: dict = None, + **kwargs, + ) -> dict: + if module == enums.ModuleID.charging_profile: + return {"result": "ACCEPTED", "timeout": 0} + return + + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ) -> dict: + if module == enums.ModuleID.sessions: + return SESSIONS[0] + return + + @classmethod + async def update( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + id, + *args, + **kwargs, + ): + return + + @classmethod + async def create( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + *args, + **kwargs, + ): + return diff --git a/tests/test_modules/test_v_2_2_1/test_commands/__init__.py b/tests/test_modules/test_v_2_2_1/test_commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_commands/conftest.py b/tests/test_modules/test_v_2_2_1/test_commands/conftest.py new file mode 100644 index 0000000..966581b --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_commands/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def command_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands, enums.ModuleID.sessions], + ) + + +@pytest.fixture +def command_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands, enums.ModuleID.sessions], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(command_cpo_v_2_2_1): + return TestClient(command_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(command_emsp_v_2_2_1): + return TestClient(command_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_commands/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_commands/test_cpo.py new file mode 100644 index 0000000..93c96a8 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_commands/test_cpo.py @@ -0,0 +1,183 @@ +import pytest + +import datetime + +from uuid import uuid4 + +from fastapi.testclient import TestClient + +from py_ocpi import get_application +from py_ocpi.core import enums +from py_ocpi.core.exceptions import NotFoundOCPIError +from py_ocpi.modules.tokens.v_2_2_1.enums import TokenType, WhitelistType +from py_ocpi.modules.commands.v_2_2_1.enums import ( + CommandType, + CommandResultType, +) +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import ( + COMMAND_RESPONSE, + COMMAND_RESULT, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, + CPO_BASE_URL, + Crud, + ClientAuthenticator, +) + +COMMAND_START_URL = f"{CPO_BASE_URL}{CommandType.start_session.value}" +COMMAND_STOP_URL = f"{CPO_BASE_URL}{CommandType.stop_session.value}" +RESERVE_NOW_URL = f"{CPO_BASE_URL}{CommandType.reserve_now.value}" + + +@pytest.mark.parametrize( + "endpoint", + [ + COMMAND_START_URL, + COMMAND_STOP_URL, + RESERVE_NOW_URL, + ], +) +def test_cpo_receive_command_start_session_not_authenticated( + client_cpo_v_2_2_1, + endpoint, +): + response = client_cpo_v_2_2_1.post( + endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_receive_command_start_session_v_2_2_1(client_cpo_v_2_2_1): + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "token": { + "country_code": "us", + "party_id": "AAA", + "uid": str(uuid4()), + "type": TokenType.rfid, + "contract_id": str(uuid4()), + "issuer": "company", + "valid": True, + "whitelist": WhitelistType.always, + "last_updated": "2022-01-02 00:00:00+00:00", + }, + "location_id": str(uuid4()), + } + + response = client_cpo_v_2_2_1.post( + COMMAND_START_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["result"] == COMMAND_RESPONSE["result"] + + +def test_cpo_receive_command_stop_session_v_2_2_1(client_cpo_v_2_2_1): + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "session_id": str(uuid4()), + } + + response = client_cpo_v_2_2_1.post( + COMMAND_STOP_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["result"] == COMMAND_RESPONSE["result"] + + +def test_cpo_receive_command_reserve_now_v_2_2_1(client_cpo_v_2_2_1): + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "token": { + "country_code": "us", + "party_id": "AAA", + "uid": str(uuid4()), + "type": TokenType.rfid, + "contract_id": str(uuid4()), + "issuer": "company", + "valid": True, + "whitelist": WhitelistType.always, + "last_updated": "2022-01-02 00:00:00+00:00", + }, + "expiry_date": str( + datetime.datetime.now() + datetime.timedelta(days=1) + ), + "reservation_id": str(uuid4()), + "location_id": str(uuid4()), + } + + response = client_cpo_v_2_2_1.post( + RESERVE_NOW_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["result"] == COMMAND_RESPONSE["result"] + + +def test_cpo_receive_command_reserve_now_unknown_location_v_2_2_1(): + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ) -> dict: + if module == enums.ModuleID.commands: + return COMMAND_RESULT + if module == enums.ModuleID.locations: + raise NotFoundOCPIError() + + _get = Crud.get + Crud.get = get + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands], + ) + + data = { + "response_url": "https://dummy.restapiexample.com/api/v1/create", + "token": { + "country_code": "us", + "party_id": "AAA", + "uid": str(uuid4()), + "type": TokenType.rfid, + "contract_id": str(uuid4()), + "issuer": "company", + "valid": True, + "whitelist": WhitelistType.always, + "last_updated": "2022-01-02 00:00:00+00:00", + }, + "expiry_date": str( + datetime.datetime.now() + datetime.timedelta(days=1) + ), + "reservation_id": str(uuid4()), + "location_id": str(uuid4()), + } + + client = TestClient(app) + response = client.post( + RESERVE_NOW_URL, + json=data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["result"] == CommandResultType.rejected + + # revert Crud changes + Crud.get = _get diff --git a/tests/test_modules/test_v_2_2_1/test_commands/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_commands/test_emsp.py new file mode 100644 index 0000000..e1f9148 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_commands/test_emsp.py @@ -0,0 +1,28 @@ +from .utils import ( + COMMAND_RESPONSE, + EMSP_BASE_URL, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, +) + +RECEIVE_URL = f"{EMSP_BASE_URL}1234" + + +def test_emsp_receive_command_result_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.post( + RECEIVE_URL, + json=COMMAND_RESPONSE, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_receive_command_result_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.post( + RECEIVE_URL, + json=COMMAND_RESPONSE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 diff --git a/tests/test_modules/test_v_2_2_1/test_commands/utils.py b/tests/test_modules/test_v_2_2_1/test_commands/utils.py new file mode 100644 index 0000000..0d52899 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_commands/utils.py @@ -0,0 +1,55 @@ +from py_ocpi.core import enums +from py_ocpi.modules.commands.v_2_2_1.enums import ( + CommandResponseType, + CommandResultType, +) + +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/commands/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/commands/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + +COMMAND_RESPONSE = {"result": CommandResponseType.accepted, "timeout": 30} + +COMMAND_RESULT = {"result": CommandResultType.accepted} + + +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 + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ) -> dict: + return COMMAND_RESULT + + @classmethod + async def update( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: dict, + id, + *args, + **kwargs, + ): + ... diff --git a/tests/test_modules/test_v_2_2_1/test_credentials/__init__.py b/tests/test_modules/test_v_2_2_1/test_credentials/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_credentials/test_credentials.py b/tests/test_modules/test_v_2_2_1/test_credentials/test_credentials.py new file mode 100644 index 0000000..f42becd --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_credentials/test_credentials.py @@ -0,0 +1,104 @@ +import functools +from uuid import uuid4 +from unittest.mock import patch +from typing import Any + +import pytest +from fastapi.testclient import TestClient +from httpx import AsyncClient + +from py_ocpi import get_application +from py_ocpi.core import enums +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.versions.enums import VersionNumber +from py_ocpi.modules.versions.schemas import Version + +from .utils import ( + Crud, + CREDENTIALS_TOKEN_CREATE, + AUTH_HEADERS, + AUTH_TOKEN_A_V_2_2_1, + AUTH_HEADERS_A, +) + +from tests.test_modules.utils import ClientAuthenticator + + +def test_cpo_get_credentials_v_2_2_1(): + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + + client = TestClient(app) + response = client.get( + "/ocpi/cpo/2.2.1/credentials", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"]["token"] == AUTH_TOKEN_A_V_2_2_1 + + +@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): + 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( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + + def override_get_versions(): + return [ + Version( + version=VersionNumber.v_2_2_1, + url=URL( + f"/{settings.OCPI_PREFIX}/{VersionNumber.v_2_2_1.value}/details" + ), + ).dict() + ] + + app_1.dependency_overrides[get_versions] = override_get_versions + + async_client.return_value = AsyncClient(app=app_1, base_url="http://test") + + app_2 = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + + 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, + headers=AUTH_HEADERS_A, + ) + + assert response.status_code == 200 + assert response.json()["data"]["token"] == CREDENTIALS_TOKEN_CREATE["token"] diff --git a/tests/test_modules/test_v_2_2_1/test_credentials/utils.py b/tests/test_modules/test_v_2_2_1/test_credentials/utils.py new file mode 100644 index 0000000..43afa75 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_credentials/utils.py @@ -0,0 +1,80 @@ +import functools +from uuid import uuid4 + +from py_ocpi.core import enums + +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_AUTH_TOKEN_A, + AUTH_TOKEN_A_V_2_2_1, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +AUTH_HEADERS_A = {"Authorization": f"Token {ENCODED_AUTH_TOKEN_A}"} +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + +CREDENTIALS_TOKEN_GET = { + "url": "url", + "roles": [ + { + "role": enums.RoleEnum.emsp, + "business_details": { + "name": "name", + }, + "party_id": "JOM", + "country_code": "MY", + } + ], +} + +CREDENTIALS_TOKEN_CREATE = { + "token": AUTH_TOKEN_A_V_2_2_1, + "url": "/ocpi/versions", + "roles": [ + { + "role": enums.RoleEnum.emsp, + "business_details": { + "name": "name", + }, + "party_id": "JOM", + "country_code": "MY", + } + ], +} + + +def partial_class(cls, *args, **kwds): + class NewCls(cls): + __init__ = functools.partialmethod(cls.__init__, *args, **kwds) + + return NewCls + + +class Crud: + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ): + return CREDENTIALS_TOKEN_CREATE + + @classmethod + async def create( + cls, module: enums.ModuleID, data, operation, *args, **kwargs + ): + if operation == "credentials": + return None + return CREDENTIALS_TOKEN_CREATE + + @classmethod + async def do( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + action: enums.Action, + *args, + data: dict = None, + **kwargs, + ): + return None diff --git a/tests/test_modules/test_v_2_2_1/test_hubclientinfo/__init__.py b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_hubclientinfo/conftest.py b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/conftest.py new file mode 100644 index 0000000..9198695 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/conftest.py @@ -0,0 +1,43 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from tests.test_modules.utils import ClientAuthenticator + +from .utils import Crud + + +@pytest.fixture +def clientinfo_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.hub_client_info], + ) + + +@pytest.fixture +def clientinfo_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.hub_client_info], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(clientinfo_cpo_v_2_2_1): + return TestClient(clientinfo_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(clientinfo_emsp_v_2_2_1): + return TestClient(clientinfo_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_cpo.py new file mode 100644 index 0000000..97236d6 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_cpo.py @@ -0,0 +1,43 @@ +from py_ocpi.core.config import settings + +from .utils import CLIENT_INFO, AUTH_HEADERS, WRONG_AUTH_HEADERS, CPO_BASE_URL + +CLIENT_INFO_URL = f"{CPO_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}" + + +def test_cpo_get_clientinfo_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get( + CLIENT_INFO_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_put_clientinfo_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.put( + CLIENT_INFO_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_clientinfo_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(CLIENT_INFO_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0] == CLIENT_INFO[0] + + +def test_cpo_add_clientinfo_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.put( + CLIENT_INFO_URL, + headers=AUTH_HEADERS, + json=CLIENT_INFO[0], + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0] == CLIENT_INFO[0] diff --git a/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_emsp.py new file mode 100644 index 0000000..207a727 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_emsp.py @@ -0,0 +1,43 @@ +from py_ocpi.core.config import settings + +from .utils import CLIENT_INFO, AUTH_HEADERS, WRONG_AUTH_HEADERS, EMSP_BASE_URL + +CLIENT_INFO_URL = f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}" + + +def test_cpo_get_clientinfo_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get( + CLIENT_INFO_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_put_clientinfo_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + CLIENT_INFO_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_clientinfo_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(CLIENT_INFO_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0] == CLIENT_INFO[0] + + +def test_cpo_add_clientinfo_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + CLIENT_INFO_URL, + headers=AUTH_HEADERS, + json=CLIENT_INFO[0], + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0] == CLIENT_INFO[0] diff --git a/tests/test_modules/test_v_2_2_1/test_hubclientinfo/utils.py b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/utils.py new file mode 100644 index 0000000..06137a8 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/utils.py @@ -0,0 +1,58 @@ +from py_ocpi.core import enums +from py_ocpi.modules.hubclientinfo.v_2_2_1.enums import ConnectionStatus +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, +) + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/clientinfo/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/clientinfo/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + + +CLIENT_INFO = [ + { + "party_id": "aaa", + "country_code": "us", + "role": enums.RoleEnum.cpo, + "status": ConnectionStatus.connected, + "last_updated": "2022-01-01 00:00:00+00:00", + } +] + + +class Crud: + @classmethod + async def get( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + id, + *args, + **kwargs, + ): + return CLIENT_INFO[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 diff --git a/tests/test_modules/test_v_2_2_1/test_locations/__init__.py b/tests/test_modules/test_v_2_2_1/test_locations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_locations/conftest.py b/tests/test_modules/test_v_2_2_1/test_locations/conftest.py new file mode 100644 index 0000000..ff44ab5 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_locations/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def location_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) + + +@pytest.fixture +def location_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(location_cpo_v_2_2_1): + return TestClient(location_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(location_emsp_v_2_2_1): + return TestClient(location_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_locations/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_locations/test_cpo.py new file mode 100644 index 0000000..2344923 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_locations/test_cpo.py @@ -0,0 +1,63 @@ +import pytest + +from .utils import LOCATIONS, AUTH_HEADERS, WRONG_AUTH_HEADERS, CPO_BASE_URL + +GET_LOCATIONS_URL = CPO_BASE_URL +GET_LOCATION_URL = f'{CPO_BASE_URL}{LOCATIONS[0]["id"]}' +GET_EVSE_URL = ( + f'{CPO_BASE_URL}{LOCATIONS[0]["id"]}' f'/{LOCATIONS[0]["evses"][0]["uid"]}' +) +GET_CONNECTOR_URL = ( + f'{CPO_BASE_URL}{LOCATIONS[0]["id"]}' + f'/{LOCATIONS[0]["evses"][0]["uid"]}' + f'/{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}' +) + + +@pytest.mark.parametrize( + "endpoint", + [ + GET_LOCATIONS_URL, + GET_LOCATION_URL, + GET_EVSE_URL, + GET_CONNECTOR_URL, + ], +) +def test_cpo_locations_not_authenticated(client_cpo_v_2_2_1, endpoint): + response = client_cpo_v_2_2_1.get(endpoint, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_cpo_get_locations_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_LOCATIONS_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_cpo_get_location_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_LOCATION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_cpo_get_evse_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_EVSE_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == LOCATIONS[0]["evses"][0]["uid"] + + +def test_cpo_get_connector_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_CONNECTOR_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["id"] + == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] + ) diff --git a/tests/test_modules/test_v_2_2_1/test_locations/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_locations/test_emsp.py new file mode 100644 index 0000000..2cc8563 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_locations/test_emsp.py @@ -0,0 +1,174 @@ +import pytest + +from uuid import uuid4 + +from py_ocpi.core.config import settings + +from .utils import EMSP_BASE_URL, AUTH_HEADERS, LOCATIONS, WRONG_AUTH_HEADERS + +LOCATION_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f"{LOCATIONS[0]['id']}" +) +EVSE_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f"{LOCATIONS[0]['id']}/{LOCATIONS[0]['evses'][0]['uid']}" +) +CONNECTOR_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f'{LOCATIONS[0]["id"]}/{LOCATIONS[0]["evses"][0]["uid"]}/' + f'{LOCATIONS[0]["evses"][0]["connectors"][0]["id"]}' +) + + +@pytest.mark.parametrize( + "endpoint", + [ + LOCATION_URL, + EVSE_URL, + CONNECTOR_URL, + ], +) +def test_emsp_get_locations_not_authenticated(client_emsp_v_2_2_1, endpoint): + response = client_emsp_v_2_2_1.get( + url=endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +@pytest.mark.parametrize( + "endpoint", + [ + LOCATION_URL, + EVSE_URL, + CONNECTOR_URL, + ], +) +def test_emsp_put_locations_not_authenticated(client_emsp_v_2_2_1, endpoint): + response = client_emsp_v_2_2_1.put( + url=endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +@pytest.mark.parametrize( + "endpoint", + [ + LOCATION_URL, + EVSE_URL, + CONNECTOR_URL, + ], +) +def test_emsp_patch_locations_not_authenticated(client_emsp_v_2_2_1, endpoint): + response = client_emsp_v_2_2_1.patch( + url=endpoint, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_location_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(LOCATION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_emsp_get_evse_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(EVSE_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == LOCATIONS[0]["evses"][0]["uid"] + + +def test_emsp_get_connector_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(CONNECTOR_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["id"] + == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] + ) + + +def test_emsp_add_location_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + LOCATION_URL, + json=LOCATIONS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == LOCATIONS[0]["id"] + + +def test_emsp_add_evse_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + EVSE_URL, + json=LOCATIONS[0]["evses"][0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == LOCATIONS[0]["evses"][0]["uid"] + + +def test_emsp_add_connector_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + CONNECTOR_URL, + json=LOCATIONS[0]["evses"][0]["connectors"][0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["id"] + == LOCATIONS[0]["evses"][0]["connectors"][0]["id"] + ) + + +def test_emsp_patch_location_v_2_2_1(client_emsp_v_2_2_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_2_1.patch( + LOCATION_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == patch_data["id"] + + +def test_emsp_patch_evse_v_2_2_1(client_emsp_v_2_2_1): + patch_data = {"uid": str(uuid4())} + response = client_emsp_v_2_2_1.patch( + EVSE_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == patch_data["uid"] + + +def test_emsp_patch_connector_v_2_2_1(client_emsp_v_2_2_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_2_1.patch( + CONNECTOR_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == patch_data["id"] diff --git a/tests/test_modules/test_v_2_2_1/test_locations/utils.py b/tests/test_modules/test_v_2_2_1/test_locations/utils.py new file mode 100644 index 0000000..23a89d9 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_locations/utils.py @@ -0,0 +1,229 @@ +from uuid import uuid4 + +from py_ocpi.core import enums +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/locations/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/locations/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + + +LOCATIONS = [ + { + "country_code": "us", + "party_id": "AAA", + "id": str(uuid4()), + "publish": True, + "publish_allowed_to": [ + { + "uid": str(uuid4()), + "type": "APP_USER", + "visual_number": "1", + "issuer": "issuer", + "group_id": "group_id", + }, + ], + "name": "name", + "address": "address", + "city": "city", + "postal_code": "111111", + "state": "state", + "country": "USA", + "coordinates": { + "latitude": "latitude", + "longitude": "longitude", + }, + "related_locations": [ + { + "latitude": "latitude", + "longitude": "longitude", + "name": {"language": "en", "text": "name"}, + }, + ], + "parking_type": "ON_STREET", + "evses": [ + { + "uid": str(uuid4()), + "evse_id": str(uuid4()), + "status": "AVAILABLE", + "status_schedule": { + "period_begin": "2022-01-01T00:00:00+00:00", + "period_end": "2022-01-01T00:00:00+00:00", + "status": "AVAILABLE", + }, + "capabilities": [ + "CREDIT_CARD_PAYABLE", + ], + "connectors": [ + { + "id": str(uuid4()), + "standard": "DOMESTIC_A", + "format": "SOCKET", + "power_type": "DC", + "max_voltage": 100, + "max_amperage": 100, + "max_electric_power": 100, + "tariff_ids": [ + str(uuid4()), + ], + "terms_and_conditions": "https://www.example.com", + "last_updated": "2022-01-01T00:00:00+00:00", + } + ], + "floor_level": "3", + "coordinates": { + "latitude": "latitude", + "longitude": "longitude", + }, + "physical_reference": "pr", + "directions": [ + {"language": "en", "text": "directions"}, + ], + "parking_restrictions": [ + "EV_ONLY", + ], + "images": [ + { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + ], + "last_updated": "2022-01-01T00:00:00+00:00", + } + ], + "directions": [ + {"language": "en", "text": "directions"}, + ], + "operator": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + }, + "suboperator": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + }, + "owner": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + }, + "facilities": ["MALL"], + "time_zone": "UTC+2", + "opening_times": { + "twentyfourseven": True, + "regular_hours": [ + { + "weekday": 1, + "period_begin": "8:00", + "period_end": "22:00", + }, + { + "weekday": 2, + "period_begin": "8:00", + "period_end": "22:00", + }, + ], + "exceptional_openings": [ + { + "period_begin": "2022-01-01T00:00:00+00:00", + "period_end": "2022-01-02T00:00:00+00:00", + }, + ], + "exceptional_closings": [], + }, + "charging_when_closed": False, + "images": [ + { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, + ], + "energy_mix": { + "is_green_energy": True, + "energy_sources": [ + {"source": "SOLAR", "percentage": 100}, + ], + "supplier_name": "supplier_name", + "energy_product_name": "energy_product_name", + }, + "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 LOCATIONS[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 list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return LOCATIONS, 1, True diff --git a/tests/test_modules/test_v_2_2_1/test_sessions/__init__.py b/tests/test_modules/test_v_2_2_1/test_sessions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_sessions/conftest.py b/tests/test_modules/test_v_2_2_1/test_sessions/conftest.py new file mode 100644 index 0000000..47d9f30 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_sessions/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def session_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.sessions], + ) + + +@pytest.fixture +def session_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.sessions], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(session_cpo_v_2_2_1): + return TestClient(session_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(session_emsp_v_2_2_1): + return TestClient(session_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_sessions/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_sessions/test_cpo.py new file mode 100644 index 0000000..7bd9ccd --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_sessions/test_cpo.py @@ -0,0 +1,52 @@ +from .utils import ( + CHARGING_PREFERENCES, + SESSIONS, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, + CPO_BASE_URL, +) + +GET_SESSION_URL = CPO_BASE_URL +PUT_SESSION_URL = f'{CPO_BASE_URL}{SESSIONS[0]["id"]}/charging_preferences' + + +def test_cpo_get_sessions_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get( + GET_SESSION_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_set_charging_preference_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.put( + PUT_SESSION_URL, + json=CHARGING_PREFERENCES, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_sessions_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_SESSION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == SESSIONS[0]["id"] + + +def test_cpo_set_charging_preference_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.put( + PUT_SESSION_URL, + json=CHARGING_PREFERENCES, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["energy_need"] + == CHARGING_PREFERENCES["energy_need"] + ) diff --git a/tests/test_modules/test_v_2_2_1/test_sessions/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_sessions/test_emsp.py new file mode 100644 index 0000000..88e4944 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_sessions/test_emsp.py @@ -0,0 +1,70 @@ +from uuid import uuid4 + +from py_ocpi.core.config import settings + +from .utils import EMSP_BASE_URL, AUTH_HEADERS, SESSIONS, WRONG_AUTH_HEADERS + +SESSION_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f'{SESSIONS[0]["id"]}' +) + + +def test_emsp_get_session_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get( + SESSION_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_add_session_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + SESSION_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_patch_session_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.patch( + SESSION_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_session_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get( + SESSION_URL, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == SESSIONS[0]["id"] + + +def test_emsp_add_session_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + SESSION_URL, + json=SESSIONS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == SESSIONS[0]["id"] + + +def test_emsp_patch_session_v_2_2_1(client_emsp_v_2_2_1): + patch_data = {"id": str(uuid4())} + response = client_emsp_v_2_2_1.patch( + SESSION_URL, + json=patch_data, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == patch_data["id"] diff --git a/tests/test_modules/test_v_2_2_1/test_sessions/utils.py b/tests/test_modules/test_v_2_2_1/test_sessions/utils.py new file mode 100644 index 0000000..4f1575f --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_sessions/utils.py @@ -0,0 +1,97 @@ +from uuid import uuid4 + +from py_ocpi.core import enums +from py_ocpi.modules.cdrs.v_2_2_1.schemas import TokenType +from py_ocpi.modules.cdrs.v_2_2_1.enums import AuthMethod, CdrDimensionType +from py_ocpi.modules.sessions.v_2_2_1.enums import SessionStatus, ProfileType + +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/sessions/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/sessions/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + +SESSIONS = [ + { + "country_code": "us", + "party_id": "AAA", + "id": str(uuid4()), + "start_date_time": "2022-01-02 00:00:00+00:00", + "end_date_time": "2022-01-02 00:05:00+00:00", + "kwh": 100, + "cdr_token": { + "country_code": "us", + "party_id": "AAA", + "uid": str(uuid4()), + "type": TokenType.rfid, + "contract_id": str(uuid4()), + }, + "auth_method": AuthMethod.auth_request, + "location_id": str(uuid4()), + "evse_uid": str(uuid4()), + "connector_id": str(uuid4()), + "currency": "MYR", + "charging_periods": [ + { + "start_date_time": "2022-01-02 00:00:00+00:00", + "dimensions": [{"type": CdrDimensionType.power, "volume": 10}], + } + ], + "total_cost": {"excl_vat": 10.0000, "incl_vat": 10.2500}, + "status": SessionStatus.active, + "last_updated": "2022-01-02 00:00:00+00:00", + } +] + +CHARGING_PREFERENCES = { + "profile_type": ProfileType.fast, + "departure_time": "2022-01-02 00:00:00+00:00", + "energy_need": 100, +} + + +class Crud: + @classmethod + async def get( + cls, module: enums.ModuleID, role: enums.RoleEnum, id, *args, **kwargs + ): + return SESSIONS[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 list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return SESSIONS, 1, True diff --git a/tests/test_modules/test_v_2_2_1/test_tariffs/__init__.py b/tests/test_modules/test_v_2_2_1/test_tariffs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_tariffs/conftest.py b/tests/test_modules/test_v_2_2_1/test_tariffs/conftest.py new file mode 100644 index 0000000..07529d1 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tariffs/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def tariff_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tariffs], + ) + + +@pytest.fixture +def tariff_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tariffs], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(tariff_cpo_v_2_2_1): + return TestClient(tariff_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(tariff_emsp_v_2_2_1): + return TestClient(tariff_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_tariffs/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_tariffs/test_cpo.py new file mode 100644 index 0000000..e871074 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tariffs/test_cpo.py @@ -0,0 +1,20 @@ +from .utils import TARIFFS, AUTH_HEADERS, WRONG_AUTH_HEADERS, CPO_BASE_URL + +GET_TARIFFS_URL = CPO_BASE_URL + + +def test_cpo_get_tariffs_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get( + GET_TARIFFS_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_tariffs_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get(GET_TARIFFS_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] diff --git a/tests/test_modules/test_v_2_2_1/test_tariffs/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_tariffs/test_emsp.py new file mode 100644 index 0000000..33b9f9a --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tariffs/test_emsp.py @@ -0,0 +1,57 @@ +from py_ocpi.core.config import settings + +from .utils import EMSP_BASE_URL, AUTH_HEADERS, TARIFFS, WRONG_AUTH_HEADERS + +TARIFF_URL = ( + f"{EMSP_BASE_URL}{settings.COUNTRY_CODE}/{settings.PARTY_ID}/" + f'{TARIFFS[0]["id"]}' +) + + +def test_emsp_get_tariff_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(TARIFF_URL, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_emsp_add_tariff_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + TARIFF_URL, + json=TARIFFS[0], + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_delete_tariff_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.delete( + TARIFF_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_emsp_get_tariff_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(TARIFF_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] + + +def test_emsp_add_tariff_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.put( + TARIFF_URL, + json=TARIFFS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert response.json()["data"][0]["id"] == TARIFFS[0]["id"] + + +def test_emsp_delete_tariff_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.delete(TARIFF_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 diff --git a/tests/test_modules/test_v_2_2_1/test_tariffs/utils.py b/tests/test_modules/test_v_2_2_1/test_tariffs/utils.py new file mode 100644 index 0000000..15cd7b6 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tariffs/utils.py @@ -0,0 +1,79 @@ +from uuid import uuid4 + +from py_ocpi.core import enums +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/tariffs/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/tariffs/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + +TARIFFS = [ + { + "country_code": "MY", + "party_id": "JOM", + "id": str(uuid4()), + "currency": "MYR", + "type": "REGULAR", + "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 diff --git a/tests/test_modules/test_v_2_2_1/test_tokens/__init__.py b/tests/test_modules/test_v_2_2_1/test_tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_tokens/conftest.py b/tests/test_modules/test_v_2_2_1/test_tokens/conftest.py new file mode 100644 index 0000000..af28adb --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tokens/conftest.py @@ -0,0 +1,41 @@ +import pytest + +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.modules.versions.enums import VersionNumber + +from .utils import Crud, ClientAuthenticator + + +@pytest.fixture +def token_cpo_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tokens], + ) + + +@pytest.fixture +def token_emsp_v_2_2_1(): + return get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.emsp], + crud=Crud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.tokens], + ) + + +@pytest.fixture +def client_cpo_v_2_2_1(token_cpo_v_2_2_1): + return TestClient(token_cpo_v_2_2_1) + + +@pytest.fixture +def client_emsp_v_2_2_1(token_emsp_v_2_2_1): + return TestClient(token_emsp_v_2_2_1) diff --git a/tests/test_modules/test_v_2_2_1/test_tokens/test_cpo.py b/tests/test_modules/test_v_2_2_1/test_tokens/test_cpo.py new file mode 100644 index 0000000..abe13ab --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tokens/test_cpo.py @@ -0,0 +1,79 @@ +from .utils import ( + TOKEN_UPDATE, + TOKENS, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, + CPO_BASE_URL, +) + +TOKEN_URL = ( + f'{CPO_BASE_URL}{TOKENS[0]["country_code"]}/{TOKENS[0]["party_id"]}/' + f'{TOKENS[0]["uid"]}' +) + + +def test_cpo_get_token_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get( + TOKEN_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_add_token_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.put( + TOKEN_URL, + json=TOKENS[0], + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_update_token_not_authenticated(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.patch( + TOKEN_URL, + json=TOKEN_UPDATE, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 403 + + +def test_cpo_get_token_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.get( + TOKEN_URL, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == TOKENS[0]["uid"] + + +def test_cpo_add_token_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.put( + TOKEN_URL, + json=TOKENS[0], + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == TOKENS[0]["uid"] + + +def test_cpo_update_token_v_2_2_1(client_cpo_v_2_2_1): + response = client_cpo_v_2_2_1.patch( + TOKEN_URL, + json=TOKEN_UPDATE, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert ( + response.json()["data"][0]["country_code"] + == TOKEN_UPDATE["country_code"] + ) diff --git a/tests/test_modules/test_v_2_2_1/test_tokens/test_emsp.py b/tests/test_modules/test_v_2_2_1/test_tokens/test_emsp.py new file mode 100644 index 0000000..a2304c9 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tokens/test_emsp.py @@ -0,0 +1,39 @@ +from py_ocpi.modules.tokens.v_2_2_1.enums import AllowedType + +from .utils import ( + EMSP_BASE_URL, + TOKENS, + AUTH_HEADERS, + WRONG_AUTH_HEADERS, +) + +GET_TOKEN = EMSP_BASE_URL +POST_TOKEN = f'{EMSP_BASE_URL}{TOKENS[0]["uid"]}/authorize' + + +def test_emsp_get_tokens_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(GET_TOKEN, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_emsp_authorize_token_not_authenticated(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.post(POST_TOKEN, headers=WRONG_AUTH_HEADERS) + + assert response.status_code == 403 + + +def test_emsp_get_tokens_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.get(GET_TOKEN, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["uid"] == TOKENS[0]["uid"] + + +def test_emsp_authorize_token_success_v_2_2_1(client_emsp_v_2_2_1): + response = client_emsp_v_2_2_1.post(POST_TOKEN, headers=AUTH_HEADERS) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["allowed"] == AllowedType.allowed diff --git a/tests/test_modules/test_v_2_2_1/test_tokens/utils.py b/tests/test_modules/test_v_2_2_1/test_tokens/utils.py new file mode 100644 index 0000000..3e00bbf --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_tokens/utils.py @@ -0,0 +1,106 @@ +from uuid import uuid4 + +from py_ocpi.core import enums +from py_ocpi.modules.cdrs.v_2_2_1.enums import AuthMethod +from py_ocpi.modules.tokens.v_2_2_1.enums import ( + WhitelistType, + TokenType, + AllowedType, +) +from py_ocpi.modules.tokens.v_2_2_1.schemas import AuthorizationInfo, Token + +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +CPO_BASE_URL = "/ocpi/cpo/2.2.1/tokens/" +EMSP_BASE_URL = "/ocpi/emsp/2.2.1/tokens/" +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} + +TOKENS = [ + { + "country_code": "us", + "party_id": "AAA", + "uid": str(uuid4()), + "type": TokenType.rfid, + "contract_id": str(uuid4()), + "issuer": "issuer", + "auth_method": AuthMethod.auth_request, + "valid": True, + "whitelist": WhitelistType.always, + "last_updated": "2022-01-02 00:00:00+00:00", + } +] + +TOKEN_UPDATE = { + "country_code": "pl", + "party_id": "BBB", + "last_updated": "2022-01-02 00:00:00+00:00", +} + + +class Crud: + @classmethod + async def get( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> Token: + return TOKENS[0] + + @classmethod + async def create( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: Token, + *args, + **kwargs, + ) -> dict: + return data + + @classmethod + async def do( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + action: enums.Action, + *args, + data: dict = None, + **kwargs, + ): + return AuthorizationInfo( + allowed=AllowedType.allowed, token=Token(**TOKENS[0]) + ).dict() + + @classmethod + async def list( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + filters: dict, + *args, + **kwargs, + ) -> list: + return TOKENS, 1, True + + @classmethod + async def update( + cls, + module: enums.ModuleID, + role: enums.RoleEnum, + data: Token, + id: str, + *args, + **kwargs, + ): + data = dict(data) + TOKENS[0]["country_code"] = data["country_code"] + TOKENS[0]["party_id"] = data["party_id"] + return TOKENS[0] diff --git a/tests/test_modules/test_v_2_2_1/test_versions/__init__.py b/tests/test_modules/test_v_2_2_1/test_versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_versions/conftest.py b/tests/test_modules/test_v_2_2_1/test_versions/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_modules/test_v_2_2_1/test_versions/test_utils.py b/tests/test_modules/test_v_2_2_1/test_versions/test_utils.py new file mode 100644 index 0000000..6093881 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_versions/test_utils.py @@ -0,0 +1,8 @@ +from tests.test_modules.utils import ( + ENCODED_AUTH_TOKEN, + ENCODED_RANDOM_AUTH_TOKEN, + ClientAuthenticator, +) + +AUTH_HEADERS = {"Authorization": f"Token {ENCODED_AUTH_TOKEN}"} +WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN}"} diff --git a/tests/test_modules/test_v_2_2_1/test_versions/test_versions.py b/tests/test_modules/test_v_2_2_1/test_versions/test_versions.py new file mode 100644 index 0000000..ee5c8c3 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_versions/test_versions.py @@ -0,0 +1,106 @@ +from fastapi.testclient import TestClient + +from py_ocpi.main import get_application +from py_ocpi.core import enums +from py_ocpi.core.crud import Crud +from py_ocpi.modules.versions.enums import VersionNumber + +from tests.test_modules.utils import AUTH_TOKEN, ClientAuthenticator +from .test_utils import AUTH_HEADERS, WRONG_AUTH_HEADERS + +VERSIONS_URL = "/ocpi/versions" +VERSION_URL = "/ocpi/2.2.1/details" + + +def test_get_versions(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSIONS_URL, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 1 + + +def test_get_versions_not_authenticated(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return None + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSIONS_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 401 + + +def test_get_versions_v_2_2_1(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSION_URL, + headers=AUTH_HEADERS, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 2 + + +def test_get_versions_v_2_2_1_not_authenticated(): + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return None + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[], + ) + client = TestClient(app) + + response = client.get( + VERSION_URL, + headers=WRONG_AUTH_HEADERS, + ) + + assert response.status_code == 401 diff --git a/tests/test_modules/test_versions.py b/tests/test_modules/test_versions.py deleted file mode 100644 index 25dde7b..0000000 --- a/tests/test_modules/test_versions.py +++ /dev/null @@ -1,52 +0,0 @@ -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.crud import Crud -from py_ocpi.core.adapter import Adapter -from py_ocpi.modules.versions.enums import VersionNumber -from py_ocpi.core.enums import ModuleID, RoleEnum, Action - - -def test_get_versions(): - - app = get_application(VersionNumber.v_2_2_1, [enums.RoleEnum.cpo], Crud, Adapter) - - client = TestClient(app) - response = client.get('/ocpi/versions') - - assert response.status_code == 200 - assert len(response.json()['data']) == 1 - - -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 == 401 diff --git a/tests/test_modules/utils.py b/tests/test_modules/utils.py new file mode 100644 index 0000000..efaa09e --- /dev/null +++ b/tests/test_modules/utils.py @@ -0,0 +1,27 @@ +from uuid import uuid4 + +from py_ocpi.core.authentication.authenticator import Authenticator +from py_ocpi.core.utils import encode_string_base64 + +AUTH_TOKEN = str(uuid4()) +AUTH_TOKEN_A = str(uuid4()) +RANDOM_AUTH_TOKEN = str(uuid4()) + +AUTH_TOKEN_V_2_2_1 = str(uuid4()) +AUTH_TOKEN_A_V_2_2_1 = str(uuid4()) +RANDOM_AUTH_TOKEN_V_2_2_1 = str(uuid4()) +ENCODED_AUTH_TOKEN = encode_string_base64(AUTH_TOKEN_V_2_2_1) +ENCODED_AUTH_TOKEN_A = encode_string_base64(AUTH_TOKEN_A_V_2_2_1) +ENCODED_RANDOM_AUTH_TOKEN = encode_string_base64(RANDOM_AUTH_TOKEN_V_2_2_1) + + +class ClientAuthenticator(Authenticator): + @classmethod + async def get_valid_token_c(cls): + """Return a list of valid tokens.""" + return [AUTH_TOKEN, AUTH_TOKEN_V_2_2_1] + + @classmethod + async def get_valid_token_a(cls): + """Return a list of valid tokens.""" + return [AUTH_TOKEN_A, AUTH_TOKEN_A_V_2_2_1] diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 9c41d74..8bef94a 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -6,18 +6,60 @@ from py_ocpi.core import enums from py_ocpi.modules.versions.enums import VersionNumber +from tests.test_modules.utils import ( + ClientAuthenticator, + ENCODED_AUTH_TOKEN, + AUTH_TOKEN, +) -def test_inject_dependency(): + +def test_inject_dependency_v_2_2_1(): + crud = AsyncMock() + crud.list.return_value = [], 0, True + + adapter = MagicMock() + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=crud, + adapter=adapter, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) + + client = TestClient(app) + response = client.get( + "/ocpi/cpo/2.2.1/locations", + headers={"Authorization": f"Token {ENCODED_AUTH_TOKEN}"}, + ) + + assert response.headers.get("X-Total-Count") == "0" + assert response.headers.get("X-Limit") == "50" + assert response.headers.get("Link") == "" + + +def test_inject_dependency_v_2_1_1(): crud = AsyncMock() crud.list.return_value = [], 0, True adapter = MagicMock() - app = get_application([VersionNumber.v_2_2_1], [enums.RoleEnum.cpo], crud, adapter) + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=crud, + adapter=adapter, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.locations], + ) client = TestClient(app) - response = client.get('/ocpi/cpo/2.2.1/locations') + response = client.get( + "/ocpi/cpo/2.1.1/locations", + headers={"Authorization": f"Token {AUTH_TOKEN}"}, + ) - assert response.headers.get('X-Total-Count') == '0' - assert response.headers.get('X-Limit') == '50' - assert response.headers.get('Link') == '' + assert response.headers.get("X-Total-Count") == "0" + assert response.headers.get("X-Limit") == "50" + assert response.headers.get("Link") == "" diff --git a/tests/test_push.py b/tests/test_push.py index 0ec9155..e61f6ff 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -7,192 +7,193 @@ from py_ocpi.core import enums, schemas from py_ocpi.modules.locations.v_2_2_1.schemas import Location from py_ocpi.modules.versions.enums import VersionNumber -from tests.test_modules.mocks.async_client import MockAsyncClientGeneratorVersionsAndEndpoints +from tests.test_modules.mocks.async_client import ( + MockAsyncClientGeneratorVersionsAndEndpoints, +) + +from tests.test_modules.utils import ( + ClientAuthenticator, + ENCODED_AUTH_TOKEN, +) LOCATIONS = [ { - 'country_code': 'us', - 'party_id': 'AAA', - 'id': str(uuid4()), - 'publish': True, - 'publish_allowed_to': [ + "country_code": "us", + "party_id": "AAA", + "id": str(uuid4()), + "publish": True, + "publish_allowed_to": [ { - 'uid': str(uuid4()), - 'type': 'APP_USER', - 'visual_number': '1', - 'issuer': 'issuer', - 'group_id': 'group_id', + "uid": str(uuid4()), + "type": "APP_USER", + "visual_number": "1", + "issuer": "issuer", + "group_id": "group_id", }, ], - 'name': 'name', - 'address': 'address', - 'city': 'city', - 'postal_code': '111111', - 'state': 'state', - 'country': 'USA', - 'coordinates': { - 'latitude': 'latitude', - 'longitude': 'longitude', + "name": "name", + "address": "address", + "city": "city", + "postal_code": "111111", + "state": "state", + "country": "USA", + "coordinates": { + "latitude": "latitude", + "longitude": "longitude", }, - 'related_locations': [ + "related_locations": [ { - 'latitude': 'latitude', - 'longitude': 'longitude', - 'name': { - 'language': 'en', - 'text': 'name' - } + "latitude": "latitude", + "longitude": "longitude", + "name": {"language": "en", "text": "name"}, }, ], - 'parking_type': 'ON_STREET', - 'evses': [ + "parking_type": "ON_STREET", + "evses": [ { - 'uid': str(uuid4()), - 'evse_id': str(uuid4()), - 'status': 'AVAILABLE', - 'status_schedule': { - 'period_begin': '2022-01-01T00:00:00+00:00', - 'period_end': '2022-01-01T00:00:00+00:00', - 'status': 'AVAILABLE' + "uid": str(uuid4()), + "evse_id": str(uuid4()), + "status": "AVAILABLE", + "status_schedule": { + "period_begin": "2022-01-01T00:00:00+00:00", + "period_end": "2022-01-01T00:00:00+00:00", + "status": "AVAILABLE", }, - 'capabilities': [ - 'CREDIT_CARD_PAYABLE', + "capabilities": [ + "CREDIT_CARD_PAYABLE", ], - 'connectors': [ + "connectors": [ { - 'id': str(uuid4()), - 'standard': 'DOMESTIC_A', - 'format': 'SOCKET', - 'power_type': 'DC', - 'max_voltage': 100, - 'max_amperage': 100, - 'max_electric_power': 100, - 'tariff_ids': [str(uuid4()), ], - 'terms_and_conditions': 'https://www.example.com', - 'last_updated': '2022-01-01T00:00:00+00:00', + "id": str(uuid4()), + "standard": "DOMESTIC_A", + "format": "SOCKET", + "power_type": "DC", + "max_voltage": 100, + "max_amperage": 100, + "max_electric_power": 100, + "tariff_ids": [ + str(uuid4()), + ], + "terms_and_conditions": "https://www.example.com", + "last_updated": "2022-01-01T00:00:00+00:00", } ], - 'floor_level': '3', - 'coordinates': { - 'latitude': 'latitude', - 'longitude': 'longitude', + "floor_level": "3", + "coordinates": { + "latitude": "latitude", + "longitude": "longitude", }, - 'physical_reference': 'pr', - 'directions': [ - { - 'language': 'en', - 'text': 'directions' - }, + "physical_reference": "pr", + "directions": [ + {"language": "en", "text": "directions"}, + ], + "parking_restrictions": [ + "EV_ONLY", ], - 'parking_restrictions': ['EV_ONLY', ], - 'images': [ + "images": [ { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, }, ], - 'last_updated': '2022-01-01T00:00:00+00:00' + "last_updated": "2022-01-01T00:00:00+00:00", } ], - 'directions': [ - { - 'language': 'en', - 'text': 'directions' - }, + "directions": [ + {"language": "en", "text": "directions"}, ], - 'operator': { - 'name': 'name', - 'website': 'https://www.example.com', - 'logo': { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - } + "operator": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, }, - 'suboperator': { - 'name': 'name', - 'website': 'https://www.example.com', - 'logo': { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - } + "suboperator": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, }, - 'owner': { - 'name': 'name', - 'website': 'https://www.example.com', - 'logo': { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 - } + "owner": { + "name": "name", + "website": "https://www.example.com", + "logo": { + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, + }, }, - 'facilities': ['MALL'], - 'time_zone': 'UTC+2', - 'opening_times': { - 'twentyfourseven': True, - 'regular_hours': [ + "facilities": ["MALL"], + "time_zone": "UTC+2", + "opening_times": { + "twentyfourseven": True, + "regular_hours": [ { - 'weekday': 1, - 'period_begin': '8:00', - 'period_end': '22:00', + "weekday": 1, + "period_begin": "8:00", + "period_end": "22:00", }, { - 'weekday': 2, - 'period_begin': '8:00', - 'period_end': '22:00', + "weekday": 2, + "period_begin": "8:00", + "period_end": "22:00", }, ], - 'exceptional_openings': [ + "exceptional_openings": [ { - 'period_begin': '2022-01-01T00:00:00+00:00', - 'period_end': '2022-01-02T00:00:00+00:00', + "period_begin": "2022-01-01T00:00:00+00:00", + "period_end": "2022-01-02T00:00:00+00:00", }, ], - 'exceptional_closings': [], + "exceptional_closings": [], }, - 'charging_when_closed': False, - 'images': [ + "charging_when_closed": False, + "images": [ { - 'url': 'https://www.example.com', - 'thumbnail': 'https://www.example.com', - 'category': 'CHARGER', - 'type': 'type', - 'width': 10, - 'height': 10 + "url": "https://www.example.com", + "thumbnail": "https://www.example.com", + "category": "CHARGER", + "type": "type", + "width": 10, + "height": 10, }, ], - 'energy_mix': { - 'is_green_energy': True, - 'energy_sources': [ - { - 'source': 'SOLAR', - 'percentage': 100 - }, + "energy_mix": { + "is_green_energy": True, + "energy_sources": [ + {"source": "SOLAR", "percentage": 100}, ], - 'supplier_name': 'supplier_name', - 'energy_product_name': 'energy_product_name' + "supplier_name": "supplier_name", + "energy_product_name": "energy_product_name", }, - 'last_updated': '2022-01-02 00:00:00+00:00', + "last_updated": "2022-01-02 00:00:00+00:00", } ] -@patch('py_ocpi.core.push.httpx.AsyncClient', - side_effect=MockAsyncClientGeneratorVersionsAndEndpoints) +@patch( + "py_ocpi.core.push.httpx.AsyncClient", + side_effect=MockAsyncClientGeneratorVersionsAndEndpoints, +) def test_push(async_client): crud = AsyncMock() adapter = MagicMock() @@ -200,20 +201,31 @@ def test_push(async_client): crud.get.return_value = LOCATIONS[0] adapter.location_adapter.return_value = Location(**LOCATIONS[0]) - app = get_application([VersionNumber.v_2_2_1], [enums.RoleEnum.cpo], crud, adapter, http_push=True) + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=crud, + adapter=adapter, + authenticator=ClientAuthenticator, + modules=[], + http_push=True, + ) client = TestClient(app) data = schemas.Push( module_id=enums.ModuleID.locations, - object_id='1', + object_id="1", receivers=[ schemas.Receiver( - endpoints_url='http://example.com', - auth_token='token' + endpoints_url="http://example.com", auth_token="token" ), - ] + ], ).dict() - client.post('/push/2.2.1', json=data) + client.post( + "/push/2.2.1", + json=data, + headers={"Authorization": f"Token {ENCODED_AUTH_TOKEN}"}, + ) crud.get.assert_awaited_once() adapter.location_adapter.assert_called_once() From 50e2a5ff06af354ea508f0f7c76e5b66a8c1509d Mon Sep 17 00:00:00 2001 From: VictorTechs <158027394+VictorTechs@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:47:23 +0100 Subject: [PATCH 2/2] GAIAG-19: hub support implementation (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: backport extrawest repo * fix: fix commit (#63) * style: fix * style: fix * fix: test, styles and security * security: fix * feat: hum support implementation --------- Co-authored-by: SergeiVorobev <45231522+SergeiVorobev@users.noreply.github.com> --- py_ocpi/modules/hubclientinfo/utils.py | 67 ++++++++++++++++++ .../test_hubclientinfo/test_hub.py | 70 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 py_ocpi/modules/hubclientinfo/utils.py create mode 100644 tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_hub.py diff --git a/py_ocpi/modules/hubclientinfo/utils.py b/py_ocpi/modules/hubclientinfo/utils.py new file mode 100644 index 0000000..33784cb --- /dev/null +++ b/py_ocpi/modules/hubclientinfo/utils.py @@ -0,0 +1,67 @@ +import requests + + +class HubTopologyHandler: + def __init__(self, hub_url, party_id, country_code, token): + self.hub_url = hub_url + self.party_id = party_id + self.country_code = country_code + self.token = token + self.headers = {"Authorization": f"Token {self.token}"} + + def register_with_hub(self): + """Register this EMSP with the OCPI Hub.""" + registration_data = { + "party_id": self.party_id, + "country_code": self.country_code, + } + response = requests.post( + f"{self.hub_url}/register", + json=registration_data, + headers=self.headers, + timeout=30 + ) + return response.json() if response.status_code == 200 else None + + def route_message(self, endpoint, payload): + """Route messages to other parties via the hub.""" + response = requests.post( # nosec + f"{self.hub_url}/{endpoint}", + json=payload, + headers=self.headers, + timeout=30 + ) + return response.json() if response.status_code == 200 else None + + def handle_incoming_message(self, payload): + """Handle incoming messages from the hub.""" + # Implement specific logic based on message type + message_type = payload.get("type") + if message_type == "COMMAND": + return self.process_command(payload) + # Other message types can be processed here + return {"status": "Unknown message type"} + + def process_command(self, payload): + """Process commands received from the hub.""" + command = payload.get("command") + # Here you would add the specific command handling logic + if command == "START_SESSION": + return self.start_session(payload) + if command == "STOP_SESSION": + return self.stop_session(payload) + return {"status": "Unknown command"} + + def start_session(self, payload): + """Handle starting a session.""" + # Process the session start, likely involves interacting with the EVSE + # or other systems. + session_id = payload.get("session_id") + # Placeholder response + return {"status": f"Session {session_id} started successfully"} + + def stop_session(self, payload): + """Handle stopping a session.""" + session_id = payload.get("session_id") + # Placeholder response + return {"status": f"Session {session_id} stopped successfully"} diff --git a/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_hub.py b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_hub.py new file mode 100644 index 0000000..a09b016 --- /dev/null +++ b/tests/test_modules/test_v_2_2_1/test_hubclientinfo/test_hub.py @@ -0,0 +1,70 @@ +import unittest +from unittest.mock import patch +from py_ocpi.modules.hubclientinfo.utils import HubTopologyHandler + + +class TestHubTopologyHandler(unittest.TestCase): + def setUp(self): + self.hub_handler = HubTopologyHandler( + hub_url="https://example-hub.com", + party_id="ESMP", + country_code="NL", + token="sample_token" + ) + + @patch("py_ocpi.modules.hubclientinfo.utils.requests.post") + def test_register_with_hub_success(self, mock_post): + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"status": "registered"} + + result = self.hub_handler.register_with_hub() + self.assertEqual(result["status"], "registered") + + @patch("py_ocpi.modules.hubclientinfo.utils.requests.post") + def test_register_with_hub_failure(self, mock_post): + mock_post.return_value.status_code = 400 + + result = self.hub_handler.register_with_hub() + self.assertIsNone(result) + + @patch("py_ocpi.modules.hubclientinfo.utils.requests.post") + def test_route_message_success(self, mock_post): + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"status": "message routed"} + + payload = {"data": "test"} + result = self.hub_handler.route_message("test_endpoint", payload) + self.assertEqual(result["status"], "message routed") + + @patch("py_ocpi.modules.hubclientinfo.utils.requests.post") + def test_route_message_failure(self, mock_post): + mock_post.return_value.status_code = 400 + + payload = {"data": "test"} + result = self.hub_handler.route_message("test_endpoint", payload) + self.assertIsNone(result) + + def test_handle_incoming_message_command(self): + payload = {"type": "COMMAND", "command": "START_SESSION", "session_id": "123"} + result = self.hub_handler.handle_incoming_message(payload) + self.assertEqual(result["status"], "Session 123 started successfully") + + def test_handle_incoming_message_unknown_type(self): + payload = {"type": "UNKNOWN"} + result = self.hub_handler.handle_incoming_message(payload) + self.assertEqual(result["status"], "Unknown message type") + + def test_process_command_start_session(self): + payload = {"command": "START_SESSION", "session_id": "123"} + result = self.hub_handler.process_command(payload) + self.assertEqual(result["status"], "Session 123 started successfully") + + def test_process_command_stop_session(self): + payload = {"command": "STOP_SESSION", "session_id": "123"} + result = self.hub_handler.process_command(payload) + self.assertEqual(result["status"], "Session 123 stopped successfully") + + def test_process_command_unknown(self): + payload = {"command": "UNKNOWN_COMMAND"} + result = self.hub_handler.process_command(payload) + self.assertEqual(result["status"], "Unknown command")