Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2025.03.31
2025.04.01
2 changes: 2 additions & 0 deletions changelog/2025-04-01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [PI-848] Add info box to swaager explaining prodID usage in non-prod envs
- [PI-870] Sonarcloud fixes
16 changes: 16 additions & 0 deletions infrastructure/swagger/02_info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ info:
name: MIT
url: https://github.com/NHSDigital/connecting-party-manager/blob/main/LICENCE.md
description: |
<div class="nhsd-m-emphasis-box nhsd-m-emphasis-box--emphasis nhsd-!t-margin-bottom-6" aria-label="Highlighted Information">
<div class="nhsd-a-box nhsd-a-box--border-blue">
<div class="nhsd-m-emphasis-box__image-box">
<figure class="nhsd-a-image">
<picture class="nhsd-a-image__picture">
<img src="http://digital.nhs.uk/binaries/content/gallery/icons/info.svg?colour=231f20" alt="" style="object-fit:fill">
</picture>
</figure>
</div>
<div class="nhsd-m-emphasis-box__content-box">
<div data-uipath="website.contentblock.emphasis.content" class="nhsd-t-word-break"><p class="nhsd-t-body">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.</p></div>
</div>
</div>
</div>
<hr class="nhsd-a-horizontal-rule">

## 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’.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
16 changes: 0 additions & 16 deletions src/api/tests/feature_tests/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 14 additions & 13 deletions src/api/tests/feature_tests/steps/context.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
24 changes: 3 additions & 21 deletions src/api/tests/feature_tests/steps/requests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json as _json
import time
from contextlib import contextmanager
from dataclasses import dataclass
from unittest import mock
Expand All @@ -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 (
Expand All @@ -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,
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions src/layers/api_utils/api_step_chain/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
}
)

Expand Down
2 changes: 1 addition & 1 deletion src/layers/api_utils/versioning/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
VERSION_RE = re.compile(r"^v(\d+)$")


class VERSIONING_STEP_ARGS:
class VersioningStepArgs:
VERSIONED_STEPS = "versioned_steps"
EVENT = "event"
8 changes: 4 additions & 4 deletions src/layers/api_utils/versioning/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]

Expand Down
8 changes: 3 additions & 5 deletions src/layers/api_utils/versioning/tests/test_steps.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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},
}
)
)
Expand All @@ -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
},
}
)
Expand Down
6 changes: 3 additions & 3 deletions src/layers/api_utils/versioning/tests/test_steps_e2e.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
11 changes: 6 additions & 5 deletions src/layers/domain/core/cpm_product/v1.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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]


Expand Down
3 changes: 2 additions & 1 deletion src/layers/domain/core/cpm_system_id/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -34,7 +35,7 @@ def _load_existing_ids():


class CpmSystemId(BaseModel, ABC):
__root__: str = None
__root__: Optional[str]

@classmethod
@abstractmethod
Expand Down
7 changes: 4 additions & 3 deletions src/layers/domain/core/product_team/v1.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from typing import Optional

from attr import dataclass
from domain.core.aggregate_root import AggregateRoot, event
Expand All @@ -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)


Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/layers/domain/core/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]+$")
1 change: 1 addition & 0 deletions src/layers/domain/repository/keys/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions src/layers/domain/request_models/v1.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/layers/domain/response/aws_lambda_response.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Literal
from typing import Literal, Optional

from pydantic import BaseModel, Field, validator

Expand All @@ -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):
Expand Down
Loading