diff --git a/CHANGELOG.md b/CHANGELOG.md index 271b6e1d3..afb42c850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2025-04-01 +- [PI-848] Add info box to swaager explaining prodID usage in non-prod envs +- [PI-870] Sonarcloud fixes + ## 2025-03-31 - [PI-835] Clean up deployment policies - [PI-861] Remove SDS secret access diff --git a/VERSION b/VERSION index 1511ca05a..38ad6010f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.03.31 +2025.04.01 diff --git a/changelog/2025-04-01.md b/changelog/2025-04-01.md new file mode 100644 index 000000000..9bfbf3efb --- /dev/null +++ b/changelog/2025-04-01.md @@ -0,0 +1,2 @@ +- [PI-848] Add info box to swaager explaining prodID usage in non-prod envs +- [PI-870] Sonarcloud fixes diff --git a/infrastructure/swagger/02_info.yaml b/infrastructure/swagger/02_info.yaml index 640641bd5..137ee3c0c 100644 --- a/infrastructure/swagger/02_info.yaml +++ b/infrastructure/swagger/02_info.yaml @@ -10,6 +10,22 @@ info: name: MIT url: https://github.com/NHSDigital/connecting-party-manager/blob/main/LICENCE.md description: | +
+
+
+
+ + + +
+
+
+

Product IDs created in non-production environments follow the same format as production, but are only valid in the environment in which they were created.

+
+
+
+
+ ## Overview Use this API to access the Connecting Party Manager (CPM) service - an internal service for registering and managing details of IT systems and applications that connect to our APIs – sometimes known as ‘connecting parties’ and referred to herein as ‘products’. diff --git a/pyproject.toml b/pyproject.toml index 1a7761c67..370da1267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "connecting-party-manager" -version = "2025.03.31" +version = "2025.04.01" description = "Repository for the Connecting Party Manager API and related services" authors = ["NHS England"] license = "LICENSE.md" diff --git a/src/api/tests/feature_tests/environment.py b/src/api/tests/feature_tests/environment.py index 02055a7a1..2931f5bce 100644 --- a/src/api/tests/feature_tests/environment.py +++ b/src/api/tests/feature_tests/environment.py @@ -82,22 +82,6 @@ def before_feature(context: Context, feature: Feature): name=feature.name, description=" ".join(feature.description), ) - cpm_scenarios = [ - "Create Product Team - success scenarios", - "Create Product Team - failure scenarios", - "Read Product Team - success scenarios", - "Read Product Team - failure scenarios", - "Delete Product Team - success scenarios", - "Delete Product Team - failure scenarios", - "Create CPM Product - success scenarios", - "Create CPM Product - failure scenarios", - "Read CPM Product - success scenarios", - "Read CPM Product - failure scenarios", - "Delete CPM Product - success scenarios", - "Delete CPM Product - failure scenarios", - "Search Products - success scenarios", - "Search Products - failures scenarios", - ] if context.test_mode is TestMode.INTEGRATION: table = "dynamodb_cpm_table_name.value" context.table_name = read_terraform_output(table) diff --git a/src/api/tests/feature_tests/steps/context.py b/src/api/tests/feature_tests/steps/context.py index 8b360061a..03cff9744 100644 --- a/src/api/tests/feature_tests/steps/context.py +++ b/src/api/tests/feature_tests/steps/context.py @@ -1,5 +1,6 @@ from contextlib import AbstractContextManager from dataclasses import dataclass +from typing import Optional from behave.model import Table from behave.runner import Context as BehaveContext @@ -17,19 +18,19 @@ @dataclass class Context(BehaveContext): base_url: str - headers: dict[str, dict[str, str]] = None - response: Response = None - table: Table = None - test_mode: TestMode = None - table_name: str = None - session: AbstractContextManager = None - dynamodb_client: DynamoDBClient = None - workspace: str = None - environment: str = None - workspace_type: str = None - api_key: str = None - notes: dict[str, str] = None - postman_endpoint: str = None + headers: Optional[dict[str, dict[str, str]]] + response: Optional[Response] + table: Optional[Table] + test_mode: Optional[TestMode] + table_name: Optional[str] + session: Optional[AbstractContextManager] + dynamodb_client: Optional[DynamoDBClient] + workspace: Optional[str] + environment: Optional[str] + workspace_type: Optional[str] + api_key: Optional[str] + notes: Optional[dict[str, str]] + postman_endpoint: Optional[str] postman_collection: PostmanCollection = None postman_feature: PostmanItem = None diff --git a/src/api/tests/feature_tests/steps/requests.py b/src/api/tests/feature_tests/steps/requests.py index 9883a7ba4..a859ece8d 100644 --- a/src/api/tests/feature_tests/steps/requests.py +++ b/src/api/tests/feature_tests/steps/requests.py @@ -1,5 +1,4 @@ import json as _json -import time from contextlib import contextmanager from dataclasses import dataclass from unittest import mock @@ -9,7 +8,6 @@ from domain.response.aws_lambda_response import AwsLambdaResponse from event.json import json_loads from requests import HTTPError, Response, request -from requests.exceptions import SSLError from api.tests.feature_tests.steps.data import DUMMY_CONTEXT from api.tests.feature_tests.steps.endpoint_lambda_mapping import ( @@ -35,21 +33,6 @@ def _parse_url(base_url: str, endpoint: str) -> str: return url -@contextmanager -def retry_on_ssl_error(sleep_time: int = 3, max_retries=5): - retries = 0 - while True: - try: - yield - except SSLError: - if retries == max_retries: - raise - time.sleep(sleep_time) - retries += 1 - finally: - break - - def make_request( base_url: str, http_method: str, @@ -62,10 +45,9 @@ def make_request( json = body if type(body) is dict else None data = None if type(body) is dict else body - with retry_on_ssl_error(): - response = request( - method=http_method, url=url, headers=headers, json=json, data=data - ) + response = request( + method=http_method, url=url, headers=headers, json=json, data=data + ) if raise_for_status: try: diff --git a/src/layers/api_utils/api_step_chain/__init__.py b/src/layers/api_utils/api_step_chain/__init__.py index a02586259..5e8c629e9 100644 --- a/src/layers/api_utils/api_step_chain/__init__.py +++ b/src/layers/api_utils/api_step_chain/__init__.py @@ -1,6 +1,6 @@ from types import ModuleType -from api_utils.versioning.constants import VERSIONING_STEP_ARGS +from api_utils.versioning.constants import VersioningStepArgs from api_utils.versioning.steps import ( get_largest_possible_version, get_steps_for_requested_version, @@ -27,8 +27,8 @@ def execute_step_chain( ) version_chain.run( init={ - VERSIONING_STEP_ARGS.EVENT: event, - VERSIONING_STEP_ARGS.VERSIONED_STEPS: versioned_steps, + VersioningStepArgs.EVENT: event, + VersioningStepArgs.VERSIONED_STEPS: versioned_steps, } ) diff --git a/src/layers/api_utils/versioning/constants.py b/src/layers/api_utils/versioning/constants.py index 7f6529ffd..b668f2673 100644 --- a/src/layers/api_utils/versioning/constants.py +++ b/src/layers/api_utils/versioning/constants.py @@ -3,6 +3,6 @@ VERSION_RE = re.compile(r"^v(\d+)$") -class VERSIONING_STEP_ARGS: +class VersioningStepArgs: VERSIONED_STEPS = "versioned_steps" EVENT = "event" diff --git a/src/layers/api_utils/versioning/steps.py b/src/layers/api_utils/versioning/steps.py index 3fe63958f..c581ea9bc 100644 --- a/src/layers/api_utils/versioning/steps.py +++ b/src/layers/api_utils/versioning/steps.py @@ -4,20 +4,20 @@ from domain.response.validation_errors import mark_validation_errors_as_inbound from event.step_chain import StepChain -from .constants import VERSIONING_STEP_ARGS +from .constants import VersioningStepArgs from .errors import VersionException from .models import Event @mark_validation_errors_as_inbound def get_requested_version(data, cache=None): - event = Event(**data[StepChain.INIT][VERSIONING_STEP_ARGS.EVENT]) + event = Event(**data[StepChain.INIT][VersioningStepArgs.EVENT]) return event.headers.version def get_largest_possible_version(data, cache=None) -> str: requested_version = data[get_requested_version] - possible_versions = data[StepChain.INIT][VERSIONING_STEP_ARGS.VERSIONED_STEPS] + possible_versions = data[StepChain.INIT][VersioningStepArgs.VERSIONED_STEPS] integer_versions = map(int, possible_versions) possible_versions = [ version @@ -31,7 +31,7 @@ def get_largest_possible_version(data, cache=None) -> str: def get_steps_for_requested_version(data, cache=None): - steps_by_version = data[StepChain.INIT][VERSIONING_STEP_ARGS.VERSIONED_STEPS] + steps_by_version = data[StepChain.INIT][VersioningStepArgs.VERSIONED_STEPS] largest_possible_version = data[get_largest_possible_version] return steps_by_version[largest_possible_version] diff --git a/src/layers/api_utils/versioning/tests/test_steps.py b/src/layers/api_utils/versioning/tests/test_steps.py index 8bc07e671..d48f6a0e3 100644 --- a/src/layers/api_utils/versioning/tests/test_steps.py +++ b/src/layers/api_utils/versioning/tests/test_steps.py @@ -1,7 +1,7 @@ from pathlib import Path import pytest -from api_utils.versioning.constants import VERSIONING_STEP_ARGS +from api_utils.versioning.constants import VersioningStepArgs from api_utils.versioning.errors import VersionException from api_utils.versioning.models import Event from api_utils.versioning.steps import ( @@ -53,9 +53,7 @@ def test_largest_possible_version(requested_version: str, expected_version: str) data=step_data( kwargs={ get_requested_version: requested_version, - StepChain.INIT: { - VERSIONING_STEP_ARGS.VERSIONED_STEPS: handler_versions - }, + StepChain.INIT: {VersioningStepArgs.VERSIONED_STEPS: handler_versions}, } ) ) @@ -72,7 +70,7 @@ def test_largest_possible_version_error(requested_version: str): kwargs={ get_requested_version: requested_version, StepChain.INIT: { - VERSIONING_STEP_ARGS.VERSIONED_STEPS: handler_versions + VersioningStepArgs.VERSIONED_STEPS: handler_versions }, } ) diff --git a/src/layers/api_utils/versioning/tests/test_steps_e2e.py b/src/layers/api_utils/versioning/tests/test_steps_e2e.py index d296bc3e8..b6da37fbd 100644 --- a/src/layers/api_utils/versioning/tests/test_steps_e2e.py +++ b/src/layers/api_utils/versioning/tests/test_steps_e2e.py @@ -1,7 +1,7 @@ from types import FunctionType import pytest -from api_utils.versioning.constants import VERSIONING_STEP_ARGS +from api_utils.versioning.constants import VersioningStepArgs from api_utils.versioning.models import Event, VersionHeader from api_utils.versioning.steps import versioning_steps from domain.logging.step_decorators import logging_step_decorators @@ -48,8 +48,8 @@ def test_versioning_steps(requested_version: str, expected_steps: list[FunctionT ) step_chain.run( init={ - VERSIONING_STEP_ARGS.EVENT: _event.dict(), - VERSIONING_STEP_ARGS.VERSIONED_STEPS: versioned_steps, + VersioningStepArgs.EVENT: _event.dict(), + VersioningStepArgs.VERSIONED_STEPS: versioned_steps, } ) assert step_chain.result is expected_steps diff --git a/src/layers/domain/core/cpm_product/v1.py b/src/layers/domain/core/cpm_product/v1.py index 3c68ecf64..df0579b8d 100644 --- a/src/layers/domain/core/cpm_product/v1.py +++ b/src/layers/domain/core/cpm_product/v1.py @@ -1,6 +1,7 @@ from datetime import datetime +from typing import Optional -from attr import dataclass +from attr import dataclass, field from domain.core.aggregate_root import UPDATED_ON, AggregateRoot, event from domain.core.cpm_system_id import ProductId from domain.core.enum import Status @@ -21,8 +22,8 @@ class CpmProductCreatedEvent(Event): ods_code: str status: Status created_on: str - updated_on: str = None - deleted_on: str = None + updated_on: Optional[str] + deleted_on: Optional[str] @dataclass(kw_only=True, slots=True) @@ -35,8 +36,8 @@ class CpmProductKeyAddedEvent(Event): ods_code: str status: Status created_on: str - updated_on: str = None - deleted_on: str = None + updated_on: Optional[str] = field(default=None) + deleted_on: Optional[str] = field(default=None) keys: list[dict] diff --git a/src/layers/domain/core/cpm_system_id/v1.py b/src/layers/domain/core/cpm_system_id/v1.py index 5ec025b0f..ca8fb1f15 100644 --- a/src/layers/domain/core/cpm_system_id/v1.py +++ b/src/layers/domain/core/cpm_system_id/v1.py @@ -5,6 +5,7 @@ from datetime import datetime from functools import cache from pathlib import Path +from typing import Optional from uuid import uuid4 from domain.core.base import BaseModel @@ -34,7 +35,7 @@ def _load_existing_ids(): class CpmSystemId(BaseModel, ABC): - __root__: str = None + __root__: Optional[str] @classmethod @abstractmethod diff --git a/src/layers/domain/core/product_team/v1.py b/src/layers/domain/core/product_team/v1.py index 7f28f9257..d1d850a70 100644 --- a/src/layers/domain/core/product_team/v1.py +++ b/src/layers/domain/core/product_team/v1.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Optional from attr import dataclass from domain.core.aggregate_root import AggregateRoot, event @@ -19,8 +20,8 @@ class ProductTeamCreatedEvent(Event): ods_code: str status: Status created_on: str - updated_on: str = None - deleted_on: str = None + updated_on: Optional[str] + deleted_on: Optional[str] keys: list[ProductTeamKey] = Field(default_factory=list) @@ -43,7 +44,7 @@ class ProductTeam(AggregateRoot): ProductTeams, meaning that `ods_code` is not unique amongst ProductTeams. """ - id: str = None + id: Optional[str] name: str = Field(regex=ENTITY_NAME_REGEX) ods_code: str status: Status = Status.ACTIVE diff --git a/src/layers/domain/core/validation.py b/src/layers/domain/core/validation.py index 76de69f88..4b3ab019d 100644 --- a/src/layers/domain/core/validation.py +++ b/src/layers/domain/core/validation.py @@ -22,4 +22,4 @@ class ProductTeamId: ) class General: - ID_PATTERN = re.compile(rf"^[a-zA-Z0-9]+$") + ID_PATTERN = re.compile(r"^[a-zA-Z0-9]+$") diff --git a/src/layers/domain/repository/keys/v1.py b/src/layers/domain/repository/keys/v1.py index fdd535c55..2fa4ff7b0 100644 --- a/src/layers/domain/repository/keys/v1.py +++ b/src/layers/domain/repository/keys/v1.py @@ -51,4 +51,5 @@ def strip_key_prefix(key: str): def remove_keys(pk=None, sk=None, pk_read=None, sk_read=None, **values): + return values diff --git a/src/layers/domain/request_models/v1.py b/src/layers/domain/request_models/v1.py index 1bc5e0668..5571e8b74 100644 --- a/src/layers/domain/request_models/v1.py +++ b/src/layers/domain/request_models/v1.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from domain.core.enum import Environment from domain.core.product_team_key import ProductTeamKey @@ -77,8 +77,8 @@ def validate_keys(cls, v: List[ProductTeamKey]) -> List[ProductTeamKey]: class SearchProductQueryParams(BaseModel, extra=Extra.forbid): - product_team_id: str = None - organisation_code: str = None + product_team_id: Optional[str] + organisation_code: Optional[str] @root_validator def check_filters(cls, values: dict): diff --git a/src/layers/domain/response/aws_lambda_response.py b/src/layers/domain/response/aws_lambda_response.py index 080607e46..b2cc4166f 100644 --- a/src/layers/domain/response/aws_lambda_response.py +++ b/src/layers/domain/response/aws_lambda_response.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Literal +from typing import Literal, Optional from pydantic import BaseModel, Field, validator @@ -23,7 +23,7 @@ class AwsLambdaResponse(BaseModel): statusCode: HTTPStatus body: str = Field(min_length=0, default="") version: None | str = Field(exclude=True) - headers: AwsLambdaResponseHeaders = None + headers: Optional[AwsLambdaResponseHeaders] @validator("headers", always=True) def generate_response_headers(cls, headers, values):