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
1,088 changes: 695 additions & 393 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ gitpython = "^3.1.44"
pytest = "^8.3.4"
pytest-asyncio = "^0.25.3"
pytest-cov = "^6.0.0"
#pytest-nhsd-apim = "^3.3.2"
pytest-nhsd-apim = "^5.0.0"
aiohttp = "^3.11.12"
awscli = "^1.37.24"
awscli-local = "^0.22.0"
Expand Down Expand Up @@ -82,6 +82,8 @@ log_cli = true
log_cli_level = "DEBUG"
log_format = "%(asctime)s %(levelname)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.run]
relative_files = true
Expand Down
5 changes: 2 additions & 3 deletions src/eligibility_signposting_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from eligibility_signposting_api import repos, services
from eligibility_signposting_api.config import config, init_logging
from eligibility_signposting_api.error_handler import handle_exception
from eligibility_signposting_api.views import eligibility, hello
from eligibility_signposting_api.views import eligibility_blueprint

init_logging()
logger = logging.getLogger(__name__)
Expand All @@ -35,8 +35,7 @@ def create_app() -> Flask:
logger.info("app created")

# Register views & error handler
app.register_blueprint(eligibility, url_prefix="/eligibility")
app.register_blueprint(hello, url_prefix="/hello")
app.register_blueprint(eligibility_blueprint, url_prefix="/eligibility")
app.register_error_handler(Exception, handle_exception)

# Set up dependency injection using wireup
Expand Down
9 changes: 9 additions & 0 deletions src/eligibility_signposting_api/model/eligibility.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
from datetime import date
from typing import NewType

from pydantic import BaseModel

NHSNumber = NewType("NHSNumber", str)
DateOfBirth = NewType("DateOfBirth", date)
Postcode = NewType("Postcode", str)


class Eligibility(BaseModel):
processed_suggestions: list[dict]
11 changes: 0 additions & 11 deletions src/eligibility_signposting_api/model/person.py

This file was deleted.

4 changes: 2 additions & 2 deletions src/eligibility_signposting_api/repos/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .eligibility_repo import EligibilityRepo
from .exceptions import NotFoundError
from .person_repo import PersonRepo

__all__ = ["NotFoundError", "PersonRepo"]
__all__ = ["EligibilityRepo", "NotFoundError"]
36 changes: 36 additions & 0 deletions src/eligibility_signposting_api/repos/eligibility_repo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging
from typing import Annotated, Any

from boto3.dynamodb.conditions import Key
from boto3.resources.base import ServiceResource
from wireup import Inject, service

from eligibility_signposting_api.model.eligibility import NHSNumber
from eligibility_signposting_api.repos.exceptions import NotFoundError

logger = logging.getLogger(__name__)


@service(qualifier="eligibility_table")
def eligibility_table_factory(dynamodb_resource: Annotated[ServiceResource, Inject(qualifier="dynamodb")]) -> Any:
table = dynamodb_resource.Table("eligibility_data_store") # type: ignore[reportAttributeAccessIssue]
logger.info("eligibility_table %r", table, extra={"table": table})
return table


@service
class EligibilityRepo:
def __init__(self, table: Annotated[Any, Inject(qualifier="eligibility_table")]) -> None:
super().__init__()
self.table = table

def get_eligibility_data(self, nhs_number: NHSNumber) -> list[dict[str, Any]]:
response = self.table.query(KeyConditionExpression=Key("NHS_NUMBER").eq(f"PERSON#{nhs_number}"))
logger.debug("response %r for %r", response, nhs_number, extra={"response": response, "nhs_number": nhs_number})

if not (items := response.get("Items")):
message = f"Person not found with nhs_number {nhs_number}"
raise NotFoundError(message)

logger.debug("returning items %s", items, extra={"items": items})
return items
41 changes: 0 additions & 41 deletions src/eligibility_signposting_api/repos/person_repo.py

This file was deleted.

4 changes: 2 additions & 2 deletions src/eligibility_signposting_api/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .person_services import PersonService, UnknownPersonError
from .eligibility_services import EligibilityService, UnknownPersonError

__all__ = ["PersonService", "UnknownPersonError"]
__all__ = ["EligibilityService", "UnknownPersonError"]
36 changes: 36 additions & 0 deletions src/eligibility_signposting_api/services/eligibility_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging

from wireup import service

from eligibility_signposting_api.model.eligibility import Eligibility, NHSNumber
from eligibility_signposting_api.repos import EligibilityRepo, NotFoundError

logger = logging.getLogger(__name__)


class UnknownPersonError(Exception):
pass


@service
class EligibilityService:
def __init__(self, eligibility_repo: EligibilityRepo) -> None:
super().__init__()
self.eligibility_repo = eligibility_repo

def get_eligibility(self, nhs_number: NHSNumber | None = None) -> Eligibility:
if nhs_number:
try:
eligibility_data = self.eligibility_repo.get_eligibility_data(nhs_number)
logger.debug(
"got eligibility_data %r",
eligibility_data,
extra={"eligibility_data": eligibility_data, "nhs_number": nhs_number},
)
except NotFoundError as e:
raise UnknownPersonError from e
else:
# TODO: Apply rules here # noqa: TD002, TD003, FIX002
return Eligibility(processed_suggestions=[])

raise UnknownPersonError
30 changes: 0 additions & 30 deletions src/eligibility_signposting_api/services/person_services.py

This file was deleted.

5 changes: 2 additions & 3 deletions src/eligibility_signposting_api/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from .eligibility import eligibility
from .hello import hello
from .eligibility import eligibility_blueprint

__all__ = ["eligibility", "hello"]
__all__ = ["eligibility_blueprint"]
32 changes: 25 additions & 7 deletions src/eligibility_signposting_api/views/eligibility.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import logging
from http import HTTPStatus

from flask import Blueprint, make_response
from flask import Blueprint, make_response, request
from flask.typing import ResponseReturnValue
from wireup import Injected

from eligibility_signposting_api.model.eligibility import NHSNumber
from eligibility_signposting_api.model.eligibility import Eligibility, NHSNumber
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
from eligibility_signposting_api.views.response_models import EligibilityResponse, Problem

logger = logging.getLogger(__name__)

eligibility = Blueprint("eligibility", __name__)
eligibility_blueprint = Blueprint("eligibility", __name__)


@eligibility.get("/<nhs_number>")
def check_eligibility(nhs_number: NHSNumber) -> ResponseReturnValue:
logger.debug("checking nhs_number %r", nhs_number, extra={"nhs_number": nhs_number})
return make_response({}, HTTPStatus.OK)
@eligibility_blueprint.get("/")
def check_eligibility(eligibility_service: Injected[EligibilityService]) -> ResponseReturnValue:
nhs_number = NHSNumber(request.args.get("nhs_number", ""))
logger.debug("checking nhs_number %r in %r", nhs_number, eligibility_service, extra={"nhs_number": nhs_number})
try:
eligibility = eligibility_service.get_eligibility(nhs_number)
except UnknownPersonError:
logger.debug("nhs_number %r not found", nhs_number, extra={"nhs_number": nhs_number})
problem = Problem(
title="nhs_number not found", status=HTTPStatus.NOT_FOUND, detail=f"nhs_number {nhs_number} not found."
)
return make_response(problem.model_dump(), HTTPStatus.NOT_FOUND)
else:
eligibility_response = build_eligibility_response(eligibility)
return make_response(eligibility_response.model_dump(), HTTPStatus.OK)


def build_eligibility_response(eligibility: Eligibility) -> EligibilityResponse:
return EligibilityResponse(processed_suggestions=eligibility.processed_suggestions)
27 changes: 0 additions & 27 deletions src/eligibility_signposting_api/views/hello.py

This file was deleted.

4 changes: 4 additions & 0 deletions src/eligibility_signposting_api/views/response_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ class Problem(BaseModel):
detail: str | None = None
instance: str | None = None
errors: list[Error] | None = None


class EligibilityResponse(BaseModel):
processed_suggestions: list[dict]
44 changes: 39 additions & 5 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
from boto3 import Session
from boto3.resources.base import ServiceResource
from botocore.client import BaseClient
from faker import Faker
from httpx import RequestError
from yarl import URL

from eligibility_signposting_api.model.eligibility import DateOfBirth, NHSNumber, Postcode

if TYPE_CHECKING:
from pytest_docker.plugin import Services

Expand All @@ -23,6 +26,11 @@
AWS_REGION = "eu-west-1"


@pytest.fixture(scope="session")
def faker() -> Faker:
return Faker("en_UK")


@pytest.fixture(scope="session")
def localstack(request: pytest.FixtureRequest) -> URL:
if url := os.getenv("RUNNING_LOCALSTACK_URL", None):
Expand Down Expand Up @@ -187,14 +195,40 @@ def wait_for_function_active(function_name, lambda_client):


@pytest.fixture(scope="session")
def people_table(dynamodb_resource: ServiceResource) -> Generator[Any]:
def eligibility_table(dynamodb_resource: ServiceResource) -> Generator[Any]:
table = dynamodb_resource.create_table(
TableName="People",
KeySchema=[{"AttributeName": "name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "name", "AttributeType": "S"}],
TableName="eligibility_data_store",
KeySchema=[
{"AttributeName": "NHS_NUMBER", "KeyType": "HASH"},
{"AttributeName": "ATTRIBUTE_TYPE", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "NHS_NUMBER", "AttributeType": "S"},
{"AttributeName": "ATTRIBUTE_TYPE", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
table.wait_until_exists()
yield table
table.delete()
table.wait_until_not_exists()


@pytest.fixture
def persisted_person(eligibility_table: Any, faker: Faker) -> Generator[tuple[NHSNumber, DateOfBirth, Postcode]]:
nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}")
date_of_birth = DateOfBirth(faker.date_of_birth())
postcode = Postcode(faker.postcode())
eligibility_table.put_item(
Item={
"NHS_NUMBER": f"PERSON#{nhs_number}",
"ATTRIBUTE_TYPE": f"PERSON#{nhs_number}",
"DATE_OF_BIRTH": date_of_birth.strftime("%Y%m%d"),
"POSTCODE": postcode,
}
)
eligibility_table.put_item(
Item={"NHS_NUMBER": f"PERSON#{nhs_number}", "ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {}}
)
yield nhs_number, date_of_birth, postcode
eligibility_table.delete_item(Key={"NHS_NUMBER": f"PERSON#{nhs_number}", "ATTRIBUTE_TYPE": f"PERSON#{nhs_number}"})
eligibility_table.delete_item(Key={"NHS_NUMBER": f"PERSON#{nhs_number}", "ATTRIBUTE_TYPE": "COHORTS"})
Loading
Loading