diff --git a/README.md b/README.md index 913c1fe55..9556fd8b3 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The first time it will also set up your pre-commit hooks. We use [Proxygen](https://github.com/NHSDigital/proxygen-cli) to deploy our proxies and specs to APIM which we have wrapped up into a make command: -- `make apigee-deploy` +- `make apigee--deploy` This when run locally will need you to have done a local `terraform--plan` and `terraform--apply` (as it will read some details from the output files) diff --git a/src/etl/sds/worker/load/tests/test_load_worker.py b/src/etl/sds/worker/load/tests/test_load_worker.py index 140e1cf0c..9d5a150b0 100644 --- a/src/etl/sds/worker/load/tests/test_load_worker.py +++ b/src/etl/sds/worker/load/tests/test_load_worker.py @@ -1,5 +1,6 @@ import os from collections import deque +from datetime import datetime from itertools import chain, permutations from typing import Callable, Generator from unittest import mock @@ -33,6 +34,9 @@ "product_team_id": str(UUID(int=1)), "ods_code": "ABC", "status": "active", + "created_on": datetime.utcnow(), + "updated_on": None, + "deleted_on": None, } } GOOD_CPM_EVENT_2 = { @@ -43,6 +47,9 @@ "product_team_id": str(UUID(int=2)), "ods_code": "ABC", "status": "active", + "created_on": datetime.utcnow(), + "updated_on": None, + "deleted_on": None, } } diff --git a/src/layers/domain/core/device.py b/src/layers/domain/core/device.py index 09b915b72..bd331942a 100644 --- a/src/layers/domain/core/device.py +++ b/src/layers/domain/core/device.py @@ -1,6 +1,8 @@ from collections import defaultdict +from datetime import datetime from enum import StrEnum, auto from itertools import chain +from typing import Optional from uuid import UUID, uuid4 from attr import dataclass, field @@ -40,6 +42,9 @@ class DeviceCreatedEvent(Event): product_team_id: UUID ods_code: str status: "DeviceStatus" + created_on: str + updated_on: Optional[str] = None + deleted_on: Optional[str] = None _trust: bool = field(alias="_trust", default=False) @@ -51,6 +56,9 @@ class DeviceUpdatedEvent(Event): product_team_id: UUID ods_code: str status: "DeviceStatus" + created_on: str + updated_on: str + deleted_on: Optional[str] = None @dataclass(kw_only=True, slots=True) @@ -149,18 +157,28 @@ class Device(AggregateRoot): status: DeviceStatus = Field(default=DeviceStatus.ACTIVE) product_team_id: UUID ods_code: str + created_on: datetime = Field(default_factory=datetime.utcnow, immutable=True) + updated_on: Optional[datetime] = Field(default=None) + deleted_on: Optional[datetime] = Field(default=None) keys: dict[str, DeviceKey] = Field(default_factory=dict, exclude=True) questionnaire_responses: dict[str, list[QuestionnaireResponse]] = Field( default_factory=lambda: defaultdict(list), exclude=True ) def update(self, **kwargs) -> DeviceUpdatedEvent: + if "updated_on" not in kwargs: + kwargs["updated_on"] = datetime.utcnow() device_data = self._update(data=kwargs) event = DeviceUpdatedEvent(**device_data) return self.add_event(event) def delete(self) -> DeviceUpdatedEvent: - return self.update(status=DeviceStatus.INACTIVE) + deletion_datetime = datetime.utcnow() + return self.update( + status=DeviceStatus.INACTIVE, + updated_on=deletion_datetime, + deleted_on=deletion_datetime, + ) def add_key(self, type: str, key: str, _trust=False) -> DeviceKeyAddedEvent: if key in self.keys: diff --git a/src/layers/domain/core/tests/test_device.py b/src/layers/domain/core/tests/test_device.py index 07d129e41..9fa7b49ac 100644 --- a/src/layers/domain/core/tests/test_device.py +++ b/src/layers/domain/core/tests/test_device.py @@ -1,3 +1,4 @@ +from datetime import datetime from itertools import chain import pytest @@ -58,15 +59,32 @@ def another_questionnaire_response() -> QuestionnaireResponse: return questionnaire.respond(responses=[{"question1": ["bye"]}]) +def test_device_created_with_datetime(device: Device): + assert isinstance(device.created_on, datetime) + assert device.updated_on == None + assert device.deleted_on == None + + def test_device_update(device: Device): + device_created_on = device.created_on + device_updated_on = device.updated_on event = device.update(name="bar") assert device.name == "bar" + assert device.deleted_on == None + assert isinstance(device.updated_on, datetime) + assert device.updated_on != device_updated_on + assert device.created_on == device_created_on assert isinstance(event, DeviceUpdatedEvent) def test_device_delete(device: Device): + device_created_on = device.created_on + assert device.deleted_on == None event = device.delete() assert device.status == DeviceStatus.INACTIVE + assert device.created_on == device_created_on + assert isinstance(device.deleted_on, datetime) + assert device.updated_on == device.deleted_on assert isinstance(event, DeviceUpdatedEvent) diff --git a/src/layers/domain/repository/marshall.py b/src/layers/domain/repository/marshall.py index 11ccf42bf..a058732fe 100644 --- a/src/layers/domain/repository/marshall.py +++ b/src/layers/domain/repository/marshall.py @@ -3,7 +3,7 @@ from .errors import UnableToUnmarshall MARSHALL_FUNCTION_BY_TYPE = { - type(None): (lambda _: {"Null": True}), + type(None): (lambda _: {"NULL": True}), bool: (lambda x: {"BOOL": x}), int: (lambda x: {"N": str(x)}), float: (lambda x: {"N": str(x)}), @@ -28,7 +28,7 @@ def _unmarshall_mapping(mapping: dict[str, dict[str, Any]]) -> dict[str, Any]: def unmarshall_value(record: dict[str, str | dict | list]): ((_type_name, value),) = record.items() match _type_name: - case "Null": + case "NULL": return None case "S": return str(value) diff --git a/src/layers/domain/repository/tests/test_marshall.py b/src/layers/domain/repository/tests/test_marshall.py index 13f40ee99..c8c099056 100644 --- a/src/layers/domain/repository/tests/test_marshall.py +++ b/src/layers/domain/repository/tests/test_marshall.py @@ -12,7 +12,7 @@ def __init__(self, depth=0): @pytest.mark.parametrize( "value,expected", [ - [None, {"Null": True}], + [None, {"NULL": True}], ["foo", {"S": "foo"}], [123, {"N": "123"}], [True, {"BOOL": True}], @@ -29,7 +29,7 @@ def __init__(self, depth=0): ], { "L": [ - {"Null": True}, + {"NULL": True}, {"N": "1"}, {"N": "2.0"}, {"S": "3"}, @@ -51,7 +51,7 @@ def __init__(self, depth=0): }, { "M": { - "none": {"Null": True}, + "none": {"NULL": True}, "bool": {"BOOL": False}, "int": {"N": "1"}, "float": {"N": "2"}, @@ -71,7 +71,7 @@ def test_marshall_value(value, expected): @pytest.mark.parametrize( "value,expected", [ - [{"Null": True}, None], + [{"NULL": True}, None], [{"BOOL": False}, False], [{"BOOL": True}, True], [{"N": "0"}, 0.0], @@ -79,7 +79,7 @@ def test_marshall_value(value, expected): [{"N": "1.2"}, 1.2], [{"S": "x"}, "x"], [{"L": []}, []], - [{"L": [{"Null": True}]}, [None]], + [{"L": [{"NULL": True}]}, [None]], [{"M": {}}, {}], [{"M": {"foo": {"BOOL": False}, "bar": {"N": "1"}}}, {"foo": False, "bar": 1}], ], diff --git a/src/layers/etl_utils/io/__init__.py b/src/layers/etl_utils/io/__init__.py index 8aa2db2aa..756107f7b 100644 --- a/src/layers/etl_utils/io/__init__.py +++ b/src/layers/etl_utils/io/__init__.py @@ -1,6 +1,7 @@ import json import pickle from collections import deque +from datetime import datetime from io import BytesIO from typing import IO from uuid import UUID @@ -18,6 +19,8 @@ def default(self, obj): return list(obj) if isinstance(obj, UUID): return str(obj) + if isinstance(obj, datetime): + return str(obj) return json.JSONEncoder.default(self, obj) diff --git a/src/layers/sds/cpm_translation/tests/test_cpm_translation.py b/src/layers/sds/cpm_translation/tests/test_cpm_translation.py index 516694332..9a8178aa7 100644 --- a/src/layers/sds/cpm_translation/tests/test_cpm_translation.py +++ b/src/layers/sds/cpm_translation/tests/test_cpm_translation.py @@ -1,3 +1,4 @@ +from datetime import datetime from itertools import chain from string import ascii_letters, digits from typing import Generator @@ -212,7 +213,14 @@ def test_delete_devices(repository: DeviceRepository): product_team_id=_device_1.product_team_id, ods_code=_device_1.ods_code, status=DeviceStatus.INACTIVE, + created_on=_device_1.created_on, + updated_on=event_1.updated_on, + deleted_on=event_1.deleted_on, ) + assert isinstance(event_1.updated_on, datetime) + assert isinstance(event_1.deleted_on, datetime) + assert event_1.deleted_on == event_1.updated_on + assert event_2 == DeviceUpdatedEvent( id=_device_2.id, name=_device_2.name, @@ -220,7 +228,13 @@ def test_delete_devices(repository: DeviceRepository): product_team_id=_device_2.product_team_id, ods_code=_device_2.ods_code, status=DeviceStatus.INACTIVE, + created_on=_device_2.created_on, + updated_on=event_2.updated_on, + deleted_on=event_2.deleted_on, ) + assert isinstance(event_2.updated_on, datetime) + assert isinstance(event_2.deleted_on, datetime) + assert event_2.deleted_on == event_2.updated_on @pytest.mark.integration