From 3c9ac26c7fac2b1da1687f2cc86c87c78f9f6528 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Thu, 28 Oct 2021 20:32:05 +0000 Subject: [PATCH 1/3] 0.41.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 4 ++++ time_tracker_api/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b29b069..8cd38258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.41.0 (2021-10-28) +### Feature +* TT-366 V2 - PUT update activity ([#331](https://github.com/ioet/time-tracker-backend/issues/331)) ([`500a5d0`](https://github.com/ioet/time-tracker-backend/commit/500a5d0261497ce9aa9a9040342fea94dbe70704)) + ## v0.40.0 (2021-10-27) ### Feature * TT-367 V2 - Delete Activity ([#330](https://github.com/ioet/time-tracker-backend/issues/330)) ([`6ba8320`](https://github.com/ioet/time-tracker-backend/commit/6ba8320c6ddd6599679dfbbbaf9ac1dba9addb8d)) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index eb9b6f12..9f86a39e 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.40.0' +__version__ = '0.41.0' From 1ed1bf0de8dc262f14ab90e51620ae8ead176c8f Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Thu, 28 Oct 2021 15:58:32 -0500 Subject: [PATCH 2/3] feat: TT-365 Method POST activity and create function serverless --- V2/serverless.yml | 12 ++++- .../azure/activity_azure_endpoints_test.py | 20 ++++++++ .../daos/activities_json_dao_test.py | 15 ++++++ .../unit/services/activity_service_test.py | 12 +++++ .../use_cases/activities_use_case_test.py | 15 +++++- V2/time_entries/_application/__init__.py | 1 + .../_application/_activities/__init__.py | 1 + .../_activities/_create_activity.py | 48 +++++++++++++++++++ .../_persistence_contracts/_activities_dao.py | 8 ++++ .../_domain/_services/_activity.py | 3 ++ .../_domain/_use_cases/__init__.py | 1 + .../_use_cases/_create_activity_use_case.py | 11 +++++ .../_data_persistence/_activities_json_dao.py | 14 +++++- .../_data_persistence/activities_data.json | 1 - V2/time_entries/interface.py | 1 + requirements/time_tracker_api/dev.txt | 1 + requirements/time_tracker_api/prod.txt | 3 +- 17 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 V2/time_entries/_application/_activities/_create_activity.py create mode 100644 V2/time_entries/_domain/_use_cases/_create_activity_use_case.py diff --git a/V2/serverless.yml b/V2/serverless.yml index 223c8a33..0d085c36 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -63,6 +63,16 @@ functions: - http: true x-azure-settings: methods: - - PUT + - PUT route: activities/{id} + authLevel: anonymous + + create_activity: + handler: time_entries/interface.create_activity + events: + - http: true + x-azure-settings: + methods: + - POST + route: activities/ authLevel: anonymous diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index ef5ba3b0..98d853f1 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -80,3 +80,23 @@ def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_act assert response.status_code == 200 assert activitiy_json_data == json.dumps(new_activity) + +def test__activity_azure_endpoint__creates_an_activity__when_activity_has_all_attributes( + create_temp_activities, + ): + activities_json, tmp_directory = create_temp_activities + activities._create_activity.JSON_PATH = tmp_directory + + activity_body = {'id': Faker().uuid4(), 'name': Faker().user_name(), 'description': Faker().sentence(),'deleted': Faker().uuid4() ,'status': 'active', 'tenant_id': Faker().uuid4()} + body = json.dumps(activity_body).encode("utf-8") + req = func.HttpRequest( + method='POST', + body= body, + url='/api/activities/', + ) + + response = activities._create_activity.create_activity(req) + activitiy_json_data = response.get_body() + + assert response.status_code == 200 + assert activitiy_json_data == body \ No newline at end of file diff --git a/V2/tests/integration/daos/activities_json_dao_test.py b/V2/tests/integration/daos/activities_json_dao_test.py index 00ce99cf..f2b0dacd 100644 --- a/V2/tests/integration/daos/activities_json_dao_test.py +++ b/V2/tests/integration/daos/activities_json_dao_test.py @@ -132,3 +132,18 @@ def test_update__returns_none__when_doesnt_found_one_activity_to_update( result = activities_json_dao.update('', activity_data) assert result == None + +def test_create_activity__returns_an_activity_dto__when_create_an_activity_that_matches_attributes(create_fake_activities): + create_fake_activities([]) + + activities_json_dao = ActivitiesJsonDao(Faker().file_path()) + activity_data = { + "name": "test_name", + "description": "test_description", + "tenant_id": "test_tenant_id", + "id": "test_id", + "deleted": "test_deleted", + "status": "test_status", + } + result = activities_json_dao.create_activity(activity_data) + assert result == Activity(**activity_data) \ No newline at end of file diff --git a/V2/tests/unit/services/activity_service_test.py b/V2/tests/unit/services/activity_service_test.py index 772b3e15..9315d24f 100644 --- a/V2/tests/unit/services/activity_service_test.py +++ b/V2/tests/unit/services/activity_service_test.py @@ -58,3 +58,15 @@ def test__update_activity__uses_the_activity_dao__to_update_one_activity( assert activity_dao.update.called assert expected_activity == updated_activity + +def test__create_activity__uses_the_activity_dao__to_create_an_activity(mocker): + expected_activity = mocker.Mock() + activity_dao = mocker.Mock( + create_activity=mocker.Mock(return_value=expected_activity) + ) + activity_service = ActivityService(activity_dao) + + actual_activity = activity_service.create_activity(Faker().pydict()) + + assert activity_dao.create_activity.called + assert expected_activity == actual_activity diff --git a/V2/tests/unit/use_cases/activities_use_case_test.py b/V2/tests/unit/use_cases/activities_use_case_test.py index f3e9a38b..793c32d6 100644 --- a/V2/tests/unit/use_cases/activities_use_case_test.py +++ b/V2/tests/unit/use_cases/activities_use_case_test.py @@ -36,6 +36,20 @@ def test__get_activity_by_id_function__uses_the_activity_service__to_retrieve_ac assert expected_activity == actual_activity +def test__create_activity_function__uses_the_activities_service__to_create_activity( + mocker: MockFixture, + ): + expected_activity = mocker.Mock() + activity_service = mocker.Mock( + create_activity=mocker.Mock(return_value=expected_activity) + ) + + activity_use_case = _use_cases.CreateActivityUseCase(activity_service) + actual_activity = activity_use_case.create_activity(fake.pydict()) + + assert activity_service.create_activity.called + assert expected_activity == actual_activity + def test__delete_activity_function__uses_the_activity_service__to_change_activity_status( mocker: MockFixture, ): @@ -50,7 +64,6 @@ def test__delete_activity_function__uses_the_activity_service__to_change_activit assert activity_service.delete.called assert expected_activity == deleted_activity - def test__update_activity_function__uses_the_activities_service__to_update_an_activity( mocker: MockFixture, ): diff --git a/V2/time_entries/_application/__init__.py b/V2/time_entries/_application/__init__.py index faa68527..c8f26492 100644 --- a/V2/time_entries/_application/__init__.py +++ b/V2/time_entries/_application/__init__.py @@ -1,3 +1,4 @@ from ._activities import get_activities from ._activities import delete_activity from ._activities import update_activity +from ._activities import create_activity diff --git a/V2/time_entries/_application/_activities/__init__.py b/V2/time_entries/_application/_activities/__init__.py index 3482a9c6..ab7d3844 100644 --- a/V2/time_entries/_application/_activities/__init__.py +++ b/V2/time_entries/_application/_activities/__init__.py @@ -1,3 +1,4 @@ from ._get_activities import get_activities from ._delete_activity import delete_activity from ._update_activity import update_activity +from ._create_activity import create_activity diff --git a/V2/time_entries/_application/_activities/_create_activity.py b/V2/time_entries/_application/_activities/_create_activity.py new file mode 100644 index 00000000..88bb8181 --- /dev/null +++ b/V2/time_entries/_application/_activities/_create_activity.py @@ -0,0 +1,48 @@ +from time_entries._infrastructure import ActivitiesJsonDao +from time_entries._domain import ActivityService, _use_cases, Activity + +import azure.functions as func +import json +import logging +import dataclasses + + +JSON_PATH = ( + 'time_entries/_infrastructure/_data_persistence/activities_data.json' +) + + + +def create_activity(req: func.HttpRequest) -> func.HttpResponse: + logging.info( + 'Python HTTP trigger function processed a request to create an activity.' + ) + activity_data = req.get_json() + status_code = 200 + if _validate_activity(activity_data): + response = _create_activity(activity_data) + else: + status_code = 404 + response = b'Not possible to create activity, attributes are not correct ' + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + +def _create_activity(activity_data: dict) -> str: + activity_use_case = _use_cases.CreateActivityUseCase( + _create_activity_service(JSON_PATH) + ) + activity = activity_use_case.create_activity(activity_data) + return json.dumps(activity.__dict__) if activity else b'Not Found' + +def _validate_activity(activity_data: dict) -> bool: + activity_keys = [field.name for field in dataclasses.fields(Activity)] + new_activity_keys = list(activity_data.keys()) + return all(map(lambda key: key in activity_keys, new_activity_keys)) and len(activity_keys) == len(new_activity_keys) + +def _create_activity_service(path: str): + activity_json = ActivitiesJsonDao(path) + return ActivityService(activity_json) + + diff --git a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py index d2f9e4c7..2037841d 100644 --- a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py +++ b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py @@ -19,3 +19,11 @@ def delete(self, id: str) -> Activity: @abc.abstractmethod def update(self, id: str, new_activity: dict) -> Activity: pass + + @abc.abstractmethod + def create_activity(self, activity_data: dict) -> Activity: + pass + + @abc.abstractmethod + def delete(self, id: str) -> Activity: + pass diff --git a/V2/time_entries/_domain/_services/_activity.py b/V2/time_entries/_domain/_services/_activity.py index f4be7836..8d29a7ab 100644 --- a/V2/time_entries/_domain/_services/_activity.py +++ b/V2/time_entries/_domain/_services/_activity.py @@ -17,3 +17,6 @@ def delete(self, activity_id: str) -> Activity: def update(self, activity_id: str, new_activity: dict) -> Activity: return self.activities_dao.update(activity_id, new_activity) + + def create_activity(self, activity_data: dict) -> Activity: + return self.activities_dao.create_activity(activity_data) diff --git a/V2/time_entries/_domain/_use_cases/__init__.py b/V2/time_entries/_domain/_use_cases/__init__.py index 64c9bb6b..642d2425 100644 --- a/V2/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_entries/_domain/_use_cases/__init__.py @@ -2,3 +2,4 @@ from ._get_activity_by_id_use_case import GetActivityUseCase from ._delete_activity_use_case import DeleteActivityUseCase from ._update_activity_use_case import UpdateActivityUseCase +from ._create_activity_use_case import CreateActivityUseCase diff --git a/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py b/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py new file mode 100644 index 00000000..a7f7a66e --- /dev/null +++ b/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py @@ -0,0 +1,11 @@ +from time_entries._domain import ActivityService, Activity +import typing + + +class CreateActivityUseCase: + def __init__(self, activity_service: ActivityService): + self.activity_service = activity_service + + def create_activity(self, activity_data: dict ) -> Activity: + return self.activity_service.create_activity(activity_data) + diff --git a/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py b/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py index dfc41d04..ab8f5765 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py +++ b/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py @@ -3,7 +3,6 @@ import json import typing - class ActivitiesJsonDao(ActivitiesDao): def __init__(self, json_data_file_path: str): self.json_data_file_path = json_data_file_path @@ -77,6 +76,19 @@ def update(self, activity_id: str, new_activity: dict) -> Activity: except FileNotFoundError: return None + def create_activity(self, activity_data: dict) -> Activity: + activities = self.__get_activities_from_file() + activities.append(activity_data) + + try: + with open(self.json_data_file_path, 'w') as outfile: + json.dump(activities, outfile) + + return self.__create_activity_dto(activity_data) + except FileNotFoundError: + print("Can not create activity") + + def __get_activities_from_file(self) -> typing.List[dict]: try: file = open(self.json_data_file_path) diff --git a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json index 0d949902..961251db 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json +++ b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json @@ -63,4 +63,3 @@ "_ts": 1632331515 } ] - diff --git a/V2/time_entries/interface.py b/V2/time_entries/interface.py index ffe31e51..1f1fc805 100644 --- a/V2/time_entries/interface.py +++ b/V2/time_entries/interface.py @@ -1,3 +1,4 @@ from ._application import get_activities from ._application import delete_activity from ._application import update_activity +from ._application import create_activity \ No newline at end of file diff --git a/requirements/time_tracker_api/dev.txt b/requirements/time_tracker_api/dev.txt index 2e5aee81..9657c071 100644 --- a/requirements/time_tracker_api/dev.txt +++ b/requirements/time_tracker_api/dev.txt @@ -6,6 +6,7 @@ # For development # Tests +Faker==4.0.2 pytest==5.2.0 Flask_sqlalchemy diff --git a/requirements/time_tracker_api/prod.txt b/requirements/time_tracker_api/prod.txt index 6fd17f94..77ed3a0a 100644 --- a/requirements/time_tracker_api/prod.txt +++ b/requirements/time_tracker_api/prod.txt @@ -3,11 +3,12 @@ # Dependencies -r ../commons.txt -r ../azure_cosmos.txt --r ../sql_db.txt +# -r ../sql_db.txt # For production releases #Required by Flask +Faker==4.0.2 Flask==1.1.1 Flask-WTF==0.15.1 flake8==3.7.9 From b7f9dd19ccdf89320d92ffca48460bb3bfb4ce51 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Fri, 29 Oct 2021 09:53:41 -0500 Subject: [PATCH 3/3] fix: TT-365 resolve comments --- .../azure/activity_azure_endpoints_test.py | 11 ++- .../_activities/_create_activity.py | 67 +++++++++++-------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index 98d853f1..05b22801 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -85,18 +85,17 @@ def test__activity_azure_endpoint__creates_an_activity__when_activity_has_all_at create_temp_activities, ): activities_json, tmp_directory = create_temp_activities - activities._create_activity.JSON_PATH = tmp_directory + activities._create_activity._JSON_PATH = tmp_directory - activity_body = {'id': Faker().uuid4(), 'name': Faker().user_name(), 'description': Faker().sentence(),'deleted': Faker().uuid4() ,'status': 'active', 'tenant_id': Faker().uuid4()} + activity_body = {'id': None, 'name': Faker().user_name(), 'description': Faker().sentence(),'deleted': Faker().uuid4() ,'status': 'active', 'tenant_id': Faker().uuid4()} body = json.dumps(activity_body).encode("utf-8") req = func.HttpRequest( method='POST', body= body, - url='/api/activities/', + url=ACTIVITY_URL, ) - response = activities._create_activity.create_activity(req) + response = activities.create_activity(req) activitiy_json_data = response.get_body() - - assert response.status_code == 200 + assert response.status_code == 201 assert activitiy_json_data == body \ No newline at end of file diff --git a/V2/time_entries/_application/_activities/_create_activity.py b/V2/time_entries/_application/_activities/_create_activity.py index 88bb8181..29594e27 100644 --- a/V2/time_entries/_application/_activities/_create_activity.py +++ b/V2/time_entries/_application/_activities/_create_activity.py @@ -1,48 +1,59 @@ -from time_entries._infrastructure import ActivitiesJsonDao -from time_entries._domain import ActivityService, _use_cases, Activity - -import azure.functions as func import json import logging import dataclasses +import typing +import azure.functions as func -JSON_PATH = ( +from ... import _domain +from ... import _infrastructure + +_JSON_PATH = ( 'time_entries/_infrastructure/_data_persistence/activities_data.json' ) - def create_activity(req: func.HttpRequest) -> func.HttpResponse: + activity_dao = _infrastructure.ActivitiesJsonDao(_JSON_PATH) + activity_service = _domain.ActivityService(activity_dao) + use_case = _domain._use_cases.CreateActivityUseCase(activity_service) logging.info( 'Python HTTP trigger function processed a request to create an activity.' ) activity_data = req.get_json() - status_code = 200 - if _validate_activity(activity_data): - response = _create_activity(activity_data) - else: - status_code = 404 - response = b'Not possible to create activity, attributes are not correct ' - - return func.HttpResponse( - body=response, status_code=status_code, mimetype="application/json" + validation_errors = _validate_activity(activity_data) + if validation_errors: + return func.HttpResponse( + body=validation_errors, status_code=400, mimetype="application/json" + ) + activity_to_create = _domain.Activity( + id= None, + name=activity_data['name'], + description=activity_data['description'], + status=activity_data['status'], + deleted=activity_data['deleted'], + tenant_id=activity_data['tenant_id'] ) -def _create_activity(activity_data: dict) -> str: - activity_use_case = _use_cases.CreateActivityUseCase( - _create_activity_service(JSON_PATH) + created_activity = use_case.create_activity(activity_to_create.__dict__) + if not create_activity: + return func.HttpResponse( + body={'error': 'activity could not be created'}, + status_code=500, + mimetype="application/json", + ) + return func.HttpResponse( + body=json.dumps(created_activity.__dict__), + status_code=201, + mimetype="application/json" ) - activity = activity_use_case.create_activity(activity_data) - return json.dumps(activity.__dict__) if activity else b'Not Found' - -def _validate_activity(activity_data: dict) -> bool: - activity_keys = [field.name for field in dataclasses.fields(Activity)] - new_activity_keys = list(activity_data.keys()) - return all(map(lambda key: key in activity_keys, new_activity_keys)) and len(activity_keys) == len(new_activity_keys) -def _create_activity_service(path: str): - activity_json = ActivitiesJsonDao(path) - return ActivityService(activity_json) +def _validate_activity(activity_data: dict) -> typing.List[str]: + activity_fields = [field.name for field in dataclasses.fields(_domain.Activity)] + missing_keys = [field for field in activity_fields if field not in activity_data] + return [ + f'The {missing_key} key is missing in the input data' + for missing_key in missing_keys + ] \ No newline at end of file