From 568e0479859b0a92cc2780fb5cb522da664c0a92 Mon Sep 17 00:00:00 2001 From: Jipson Murillo <38593785+Jobzi@users.noreply.github.com> Date: Thu, 11 Nov 2021 15:40:31 -0500 Subject: [PATCH 01/18] feat: TT-384 Refactor Tables (#337) * feat: file stream from azure blob storage * refactor: add new python package in dev.txt * feat: implement new methods to read files from blob storage * feat: implemented the reading of the blob storage to the endpoint activity * fix: TT-384 Change blob storage connection input names * fix: TT-384 Add the file name as a parameter of the function * test: TT-384 Add a tests to obtain activities from blob storage, endpoint and repository * fix: TT-384 revert changes * test: TT-384 Change blob storage connection input names * feat: TT-384 implemented the reading of the storage blob to the endpoint and repository * test: TT-384 Add a tests to obtain activities from blob storage, endpoint and repository * test: TT-384 changed test name with correct formatting * refactor: TT-384 change import to global and name method --- commons/data_access_layer/file_stream.py | 27 +++++++++++++++++++ requirements/time_tracker_api/dev.txt | 5 +++- .../data_access_layer/file_stream_test.py | 15 +++++++++++ .../activities/activities_model_test.py | 24 +++++++++++++++++ .../activities/activities_namespace_test.py | 13 +++++++-- .../activities/activities_model.py | 24 +++++++++++++++-- 6 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 commons/data_access_layer/file_stream.py create mode 100644 tests/commons/data_access_layer/file_stream_test.py diff --git a/commons/data_access_layer/file_stream.py b/commons/data_access_layer/file_stream.py new file mode 100644 index 00000000..a705c061 --- /dev/null +++ b/commons/data_access_layer/file_stream.py @@ -0,0 +1,27 @@ +import os +from azure.storage.blob.blockblobservice import BlockBlobService + +ACCOUNT_KEY = os.environ.get('AZURE_STORAGE_ACCOUNT_KEY') + +class FileStream: + def __init__(self, account_name:str, container_name:str): + """ + Initialize the FileStream object. which is used to get the file stream from Azure Blob Storage. + `account_name`: The name of the Azure Storage account. + `container_name`: The name of the Azure Storage container. + """ + self.account_name = account_name + self.container_name = container_name + self.blob_service = BlockBlobService(account_name=self.account_name, account_key=ACCOUNT_KEY) + + def get_file_stream(self, filename:str): + import tempfile + try: + local_file = tempfile.NamedTemporaryFile() + self.blob_service.get_blob_to_stream(self.container_name, filename, stream=local_file) + + local_file.seek(0) + return local_file + except Exception as e: + print(e) + return None \ No newline at end of file diff --git a/requirements/time_tracker_api/dev.txt b/requirements/time_tracker_api/dev.txt index 9657c071..b7a6d667 100644 --- a/requirements/time_tracker_api/dev.txt +++ b/requirements/time_tracker_api/dev.txt @@ -19,4 +19,7 @@ coverage==4.5.1 # CLI tools PyInquirer==1.0.3 pyfiglet==0.7 -factory_boy==3.2.0 \ No newline at end of file +factory_boy==3.2.0 + +# azure blob storage +azure-storage-blob==2.1.0 \ No newline at end of file diff --git a/tests/commons/data_access_layer/file_stream_test.py b/tests/commons/data_access_layer/file_stream_test.py new file mode 100644 index 00000000..a3119774 --- /dev/null +++ b/tests/commons/data_access_layer/file_stream_test.py @@ -0,0 +1,15 @@ +import json + +from commons.data_access_layer.file_stream import FileStream + +fs = FileStream("storageaccounteystr82c5","tt-common-files") + +def test__get_file_stream__return_file_content__when_enter_file_name(): + result = fs.get_file_stream("activity_test.json") + + assert len(json.load(result)) == 15 + +def test__get_file_stream__return_None__when_not_enter_file_name_or_incorrect_name(): + result = fs.get_file_stream("") + + assert result == None \ No newline at end of file diff --git a/tests/time_tracker_api/activities/activities_model_test.py b/tests/time_tracker_api/activities/activities_model_test.py index c1a1b243..66e08ed7 100644 --- a/tests/time_tracker_api/activities/activities_model_test.py +++ b/tests/time_tracker_api/activities/activities_model_test.py @@ -64,3 +64,27 @@ def test_create_activity_should_add_active_status( activity_repository_create_mock.assert_called_with( data=expect_argument, event_context=ANY ) + +def test__find_all_from_blob_storage__return_list__when_send_event_context_and_correct_file_name( + event_context: EventContext, + activity_repository: ActivityCosmosDBRepository, +): + activity_repository.container = Mock() + + result = activity_repository.find_all_from_blob_storage( + event_context=event_context, + file_name="activity_test.json" + ) + assert len(result) == 15 + +def test__find_all_from_blob_storage__return_empty_list__when_send_event_context_and_incorrect_file_name( + event_context: EventContext, + activity_repository: ActivityCosmosDBRepository, +): + activity_repository.container = Mock() + + result = activity_repository.find_all_from_blob_storage( + event_context=event_context, + file_name="incorrect.json" + ) + assert result == [] \ No newline at end of file diff --git a/tests/time_tracker_api/activities/activities_namespace_test.py b/tests/time_tracker_api/activities/activities_namespace_test.py index a2b9ab20..86e34691 100644 --- a/tests/time_tracker_api/activities/activities_namespace_test.py +++ b/tests/time_tracker_api/activities/activities_namespace_test.py @@ -4,6 +4,7 @@ from flask import json from flask.testing import FlaskClient from flask_restplus._http import HTTPStatus +import pytest from pytest_mock import MockFixture from utils.enums.status import Status @@ -18,6 +19,14 @@ fake_activity = ({"id": fake.random_int(1, 9999)}).update(valid_activity_data) +def test__get_all_activities__return_response__when_send_activities_get_request( + client: FlaskClient, valid_header: dict +): + response = client.get( + "/activities", headers=valid_header, follow_redirects=True + ) + + assert HTTPStatus.OK == response.status_code def test_create_activity_should_succeed_with_valid_request( client: FlaskClient, mocker: MockFixture, valid_header: dict @@ -55,7 +64,7 @@ def test_create_activity_should_reject_bad_request( assert HTTPStatus.BAD_REQUEST == response.status_code repository_create_mock.assert_not_called() - +@pytest.mark.skip(reason="There is currently no way to test this. Getting the value of the azure blob storage") def test_list_all_active( client: FlaskClient, mocker: MockFixture, valid_header: dict ): @@ -81,7 +90,7 @@ def test_list_all_active( max_count=ANY, ) - +@pytest.mark.skip(reason="There is currently no way to test this. Getting the value of the azure blob storage") def test_list_all_active_activities( client: FlaskClient, mocker: MockFixture, valid_header: dict ): diff --git a/time_tracker_api/activities/activities_model.py b/time_tracker_api/activities/activities_model.py index cbfd0d20..ddb46411 100644 --- a/time_tracker_api/activities/activities_model.py +++ b/time_tracker_api/activities/activities_model.py @@ -1,5 +1,6 @@ from dataclasses import dataclass +import json from azure.cosmos import PartitionKey from commons.data_access_layer.cosmos_db import ( @@ -12,7 +13,7 @@ from commons.data_access_layer.database import EventContext from utils.enums.status import Status from utils.query_builder import CosmosDBQueryBuilder - +from commons.data_access_layer.file_stream import FileStream class ActivityDao(CRUDDao): pass @@ -113,6 +114,20 @@ def find_all( function_mapper = self.get_mapper_or_dict(mapper) return list(map(function_mapper, result)) + def find_all_from_blob_storage( + self, + event_context: EventContext, + mapper: Callable = None, + file_name: str = "activity.json", + ): + tenant_id_value = self.find_partition_key_value(event_context) + function_mapper = self.get_mapper_or_dict(mapper) + if tenant_id_value is None: + return [] + + fs = FileStream("storageaccounteystr82c5","tt-common-files") + result = fs.get_file_stream(file_name) + return list(map(function_mapper, json.load(result))) if result is not None else [] class ActivityCosmosDBDao(APICosmosDBDao, ActivityDao): def __init__(self, repository): @@ -128,7 +143,7 @@ def get_all_with_id_in_list( activity_ids, ) - def get_all( + def get_all_v1( self, conditions: dict = None, activities_id: List = None, @@ -147,6 +162,11 @@ def get_all( ) return activities + def get_all(self, conditions: dict = None) -> list: + event_ctx = self.create_event_context("read-many") + activities = self.repository.find_all_from_blob_storage(event_context=event_ctx) + return activities + def create(self, activity_payload: dict): event_ctx = self.create_event_context('create') activity_payload['status'] = Status.ACTIVE.value From 9be546f4e4c225795ae4deccf291183a0fb82557 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Thu, 11 Nov 2021 21:11:17 +0000 Subject: [PATCH 02/18] 0.43.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 5cfb3855..0aa521ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.43.0 (2021-11-11) +### Feature +* TT-384 Refactor Tables ([#337](https://github.com/ioet/time-tracker-backend/issues/337)) ([`568e047`](https://github.com/ioet/time-tracker-backend/commit/568e0479859b0a92cc2780fb5cb522da664c0a92)) + ## v0.42.1 (2021-11-04) ### Fix * TT-365 v2 post method fix ([#333](https://github.com/ioet/time-tracker-backend/issues/333)) ([`cb892c3`](https://github.com/ioet/time-tracker-backend/commit/cb892c338c1139640a5527772b398b3b34ff68a7)) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 3861aea9..1e79165d 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.42.1' +__version__ = '0.43.0' From 6e2108ee03dcfd48fa9676a69591248a2467f27c Mon Sep 17 00:00:00 2001 From: mandres2015 <32377408+mandres2015@users.noreply.github.com> Date: Fri, 12 Nov 2021 11:15:14 -0500 Subject: [PATCH 03/18] fix: TT-393 userid convert to list (#339) * feat: TT-365 Method POST activity and create function serverless * fix: TT-393 change user id variable to list * TT-393 added list to userid * TT-393 added list to userid * TT-393 resolve comment Co-authored-by: Sandro Castillo Co-authored-by: Daniela Garcia --- package-lock.json | 6 ++++++ tests/utils/azure_users_test.py | 2 +- time-tracker.sh | 0 utils/azure_users.py | 3 ++- 4 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 package-lock.json mode change 100644 => 100755 time-tracker.sh diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..1231a8ae --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "time-tracker-backend", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/tests/utils/azure_users_test.py b/tests/utils/azure_users_test.py index 49d99f9d..22bd8965 100644 --- a/tests/utils/azure_users_test.py +++ b/tests/utils/azure_users_test.py @@ -141,7 +141,7 @@ def test_get_groups_and_users(get_mock): get_mock.return_value = response_mock expected_result = [ - ('test-group-1', ['user-id1', 'user-id2', MSConfig.USERID]), + ('test-group-1', ['user-id1', 'user-id2', *MSConfig.USERID.split(",")]), ('test-group-2', ['user-id3', 'user-id1']), ('test-group-3', []), ] diff --git a/time-tracker.sh b/time-tracker.sh old mode 100644 new mode 100755 diff --git a/utils/azure_users.py b/utils/azure_users.py index ba271a4d..45a1a0f3 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -263,7 +263,8 @@ def get_groups_and_users(self): [member['objectId'] for member in item['members']], ) result = list(map(parse_item, response.json()['value'])) - result[0][1].append(self.config.USERID) + users_id = self.config.USERID.split(",") + result[0][1].extend(users_id) return result From a20bfe0b9239cc7adac4cb569338da6ea3a20e21 Mon Sep 17 00:00:00 2001 From: Jipson Murillo <38593785+Jobzi@users.noreply.github.com> Date: Fri, 12 Nov 2021 11:21:14 -0500 Subject: [PATCH 04/18] fix:TT-384 add package blob storage to prod.txt (#343) * feat: file stream from azure blob storage * refactor: add new python package in dev.txt * feat: implement new methods to read files from blob storage * feat: implemented the reading of the blob storage to the endpoint activity * fix: TT-384 Change blob storage connection input names * fix: TT-384 Add the file name as a parameter of the function * test: TT-384 Add a tests to obtain activities from blob storage, endpoint and repository * fix: TT-384 revert changes * test: TT-384 Change blob storage connection input names * feat: TT-384 implemented the reading of the storage blob to the endpoint and repository * test: TT-384 Add a tests to obtain activities from blob storage, endpoint and repository * test: TT-384 changed test name with correct formatting * refactor: TT-384 change import to global and name method * refactor: change import json to global * fix: TT-384 add package azure blob storage to prod.txt --- requirements/time_tracker_api/prod.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements/time_tracker_api/prod.txt b/requirements/time_tracker_api/prod.txt index 77ed3a0a..dd6df0df 100644 --- a/requirements/time_tracker_api/prod.txt +++ b/requirements/time_tracker_api/prod.txt @@ -44,4 +44,7 @@ azure-functions-worker==1.1.9 # Time utils pytz==2019.3 -python-dateutil==2.8.1 \ No newline at end of file +python-dateutil==2.8.1 + +# azure blob storage +azure-storage-blob==2.1.0 \ No newline at end of file From 2f1504146513316b60f74e5756f45e356919e591 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 12 Nov 2021 17:07:23 +0000 Subject: [PATCH 05/18] 0.43.1 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 0aa521ef..de620596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.43.1 (2021-11-12) +### Fix +* TT-393 userid convert to list ([#339](https://github.com/ioet/time-tracker-backend/issues/339)) ([`6e2108e`](https://github.com/ioet/time-tracker-backend/commit/6e2108ee03dcfd48fa9676a69591248a2467f27c)) + ## v0.43.0 (2021-11-11) ### Feature * TT-384 Refactor Tables ([#337](https://github.com/ioet/time-tracker-backend/issues/337)) ([`568e047`](https://github.com/ioet/time-tracker-backend/commit/568e0479859b0a92cc2780fb5cb522da664c0a92)) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 1e79165d..d5f90b8c 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.43.0' +__version__ = '0.43.1' From 80c256ae554614ff1b13ed606b1e4598da2eed9d Mon Sep 17 00:00:00 2001 From: Jipson Murillo <38593785+Jobzi@users.noreply.github.com> Date: Sat, 13 Nov 2021 14:57:16 -0500 Subject: [PATCH 06/18] test: TT-384 revert to origin get_all (#345) --- time_tracker_api/activities/activities_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time_tracker_api/activities/activities_model.py b/time_tracker_api/activities/activities_model.py index ddb46411..158c8053 100644 --- a/time_tracker_api/activities/activities_model.py +++ b/time_tracker_api/activities/activities_model.py @@ -143,7 +143,7 @@ def get_all_with_id_in_list( activity_ids, ) - def get_all_v1( + def get_all( self, conditions: dict = None, activities_id: List = None, @@ -162,7 +162,7 @@ def get_all_v1( ) return activities - def get_all(self, conditions: dict = None) -> list: + def get_all_test(self, conditions: dict = None) -> list: event_ctx = self.create_event_context("read-many") activities = self.repository.find_all_from_blob_storage(event_context=event_ctx) return activities From 80f4ed136b81c14f4265384bdd888bff2b3c6206 Mon Sep 17 00:00:00 2001 From: Jipson Murillo <38593785+Jobzi@users.noreply.github.com> Date: Mon, 15 Nov 2021 09:10:10 -0500 Subject: [PATCH 07/18] test: TT-384 get all activities from blob storage (#348) --- time_tracker_api/activities/activities_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time_tracker_api/activities/activities_model.py b/time_tracker_api/activities/activities_model.py index 158c8053..ddb46411 100644 --- a/time_tracker_api/activities/activities_model.py +++ b/time_tracker_api/activities/activities_model.py @@ -143,7 +143,7 @@ def get_all_with_id_in_list( activity_ids, ) - def get_all( + def get_all_v1( self, conditions: dict = None, activities_id: List = None, @@ -162,7 +162,7 @@ def get_all( ) return activities - def get_all_test(self, conditions: dict = None) -> list: + def get_all(self, conditions: dict = None) -> list: event_ctx = self.create_event_context("read-many") activities = self.repository.find_all_from_blob_storage(event_context=event_ctx) return activities From 3a99add39a3130c540d86b02c5a69dbda8536e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rafael=20Arcos=20G=C3=B3mez?= <37599693+ararcos@users.noreply.github.com> Date: Mon, 15 Nov 2021 09:53:25 -0500 Subject: [PATCH 08/18] feat: TT-357 Create V2 Activities Azure DAO (#334) * feat: TT-357 Change Json Implementation for SQL * fix: TT-357 Resolution of comments * fix: TT-357 Update requirements * Refactor: TT-357 correction of FlakeV8 * fix: TT-357 change of an environment variable to a constant * refactor: TT-357 Refactor update and create activity Co-authored-by: Daniela Garcia --- V2/.flake8 | 2 +- V2/Makefile | 3 +- V2/create_activity/function.json | 22 +++ V2/delete_activity/function.json | 22 +++ V2/docker-compose.yml | 10 ++ V2/get_activities/function.json | 22 +++ V2/requirements.txt | 6 +- V2/tests/api/api_fixtures.py | 41 ----- .../azure/activity_azure_endpoints_test.py | 129 +++++++++------ V2/tests/conftest.py | 2 +- V2/tests/fixtures.py | 35 ++++ .../daos/activities_json_dao_test.py | 152 ------------------ .../daos/activities_sql_dao_test.py | 138 ++++++++++++++++ .../unit/services/activity_service_test.py | 8 +- .../use_cases/activities_use_case_test.py | 30 ++-- V2/time_tracker/_infrastructure/__init__.py | 3 + V2/time_tracker/_infrastructure/_config.py | 20 +++ V2/time_tracker/_infrastructure/_db.py | 20 +++ .../_activities/_create_activity.py | 12 +- .../_activities/_delete_activity.py | 49 +++--- .../_activities/_get_activities.py | 59 +++---- .../_activities/_update_activity.py | 70 ++++---- .../activities/_domain/_entities/_activity.py | 7 +- .../_persistence_contracts/_activities_dao.py | 8 +- .../activities/_domain/_services/_activity.py | 12 +- .../_use_cases/_create_activity_use_case.py | 4 +- .../_use_cases/_delete_activity_use_case.py | 2 +- .../_get_activity_by_id_use_case.py | 2 +- .../_use_cases/_update_activity_use_case.py | 4 +- .../activities/_infrastructure/__init__.py | 2 +- .../_data_persistence/__init__.py | 2 +- .../_data_persistence/_activities_json_dao.py | 105 ------------ .../_data_persistence/_activities_sql_dao.py | 67 ++++++++ .../_data_persistence/activities_data.json | 65 -------- V2/time_tracker/activities/interface.py | 2 +- V2/update_activity/function.json | 22 +++ 36 files changed, 617 insertions(+), 542 deletions(-) create mode 100644 V2/create_activity/function.json create mode 100644 V2/delete_activity/function.json create mode 100644 V2/docker-compose.yml create mode 100644 V2/get_activities/function.json delete mode 100644 V2/tests/api/api_fixtures.py create mode 100644 V2/tests/fixtures.py delete mode 100644 V2/tests/integration/daos/activities_json_dao_test.py create mode 100644 V2/tests/integration/daos/activities_sql_dao_test.py create mode 100644 V2/time_tracker/_infrastructure/__init__.py create mode 100644 V2/time_tracker/_infrastructure/_config.py create mode 100644 V2/time_tracker/_infrastructure/_db.py delete mode 100644 V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_json_dao.py create mode 100644 V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py delete mode 100644 V2/time_tracker/activities/_infrastructure/_data_persistence/activities_data.json create mode 100644 V2/update_activity/function.json diff --git a/V2/.flake8 b/V2/.flake8 index cb282cae..ecba83ba 100644 --- a/V2/.flake8 +++ b/V2/.flake8 @@ -1,4 +1,4 @@ [flake8] -exclude = .git,__pycache__,./node_modules, +exclude = .git,__pycache__,./node_modules,.venv max-complexity = 10 max_line_length = 120 \ No newline at end of file diff --git a/V2/Makefile b/V2/Makefile index 9a0956ba..45080238 100644 --- a/V2/Makefile +++ b/V2/Makefile @@ -4,4 +4,5 @@ install: pip install --upgrade pip pip install -r requirements.txt @echo "Completed! " - +start-local: + docker compose up \ No newline at end of file diff --git a/V2/create_activity/function.json b/V2/create_activity/function.json new file mode 100644 index 00000000..ed3454a9 --- /dev/null +++ b/V2/create_activity/function.json @@ -0,0 +1,22 @@ +{ + "disabled": false, + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "activities/", + "authLevel": "anonymous", + "methods": [ + "POST" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ], + "entryPoint": "create_activity", + "scriptFile": "../time_tracker/activities/interface.py" +} \ No newline at end of file diff --git a/V2/delete_activity/function.json b/V2/delete_activity/function.json new file mode 100644 index 00000000..d51170fd --- /dev/null +++ b/V2/delete_activity/function.json @@ -0,0 +1,22 @@ +{ + "disabled": false, + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "activities/{id}", + "authLevel": "anonymous", + "methods": [ + "DELETE" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ], + "entryPoint": "delete_activity", + "scriptFile": "../time_tracker/activities/interface.py" +} \ No newline at end of file diff --git a/V2/docker-compose.yml b/V2/docker-compose.yml new file mode 100644 index 00000000..a89f5250 --- /dev/null +++ b/V2/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.9' +services: + database: + image: postgres:14 + ports: + - "5433:5432" + environment: + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASS} + - POSTGRES_DB=${DB_NAME} \ No newline at end of file diff --git a/V2/get_activities/function.json b/V2/get_activities/function.json new file mode 100644 index 00000000..ee1efe53 --- /dev/null +++ b/V2/get_activities/function.json @@ -0,0 +1,22 @@ +{ + "disabled": false, + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "activities/{id:?}", + "authLevel": "anonymous", + "methods": [ + "GET" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ], + "entryPoint": "get_activities", + "scriptFile": "../time_tracker/activities/interface.py" +} \ No newline at end of file diff --git a/V2/requirements.txt b/V2/requirements.txt index c651bb35..8be0a2a8 100644 --- a/V2/requirements.txt +++ b/V2/requirements.txt @@ -10,4 +10,8 @@ flake8==4.0.1 pytest-mock # To create sample content in tests and API documentation -Faker==4.0.2 \ No newline at end of file +Faker==4.0.2 + +#SQL ALCHEMY +SQLAlchemy==1.4.24 +psycopg2==2.9.1 \ No newline at end of file diff --git a/V2/tests/api/api_fixtures.py b/V2/tests/api/api_fixtures.py deleted file mode 100644 index 21b58021..00000000 --- a/V2/tests/api/api_fixtures.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -import pytest -import shutil - - -@pytest.fixture -def create_temp_activities(tmpdir_factory): - temporary_directory = tmpdir_factory.mktemp("tmp") - json_file = temporary_directory.join("activities.json") - activities = [ - { - 'id': 'c61a4a49-3364-49a3-a7f7-0c5f2d15072b', - 'name': 'Development', - 'description': 'Development', - 'deleted': 'b4327ba6-9f96-49ee-a9ac-3c1edf525172', - 'status': 'active', - 'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05', - }, - { - 'id': '94ec92e2-a500-4700-a9f6-e41eb7b5507c', - 'name': 'Management', - 'description': 'Description of management', - 'deleted': '7cf6efe5-a221-4fe4-b94f-8945127a489a', - 'status': 'active', - 'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05', - }, - { - 'id': 'd45c770a-b1a0-4bd8-a713-22c01a23e41b', - 'name': 'Operations', - 'description': 'Operation activities performed.', - 'deleted': '7cf6efe5-a221-4fe4-b94f-8945127a489a', - 'status': 'active', - 'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05', - }, - ] - - with open(json_file, 'w') as outfile: - json.dump(activities, outfile) - - yield activities, json_file - shutil.rmtree(temporary_directory) diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index e3bf4ffe..9b2618a8 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -1,108 +1,135 @@ -from time_tracker.activities._application import _activities as activities +import pytest +import json from faker import Faker import azure.functions as func -import json +import time_tracker.activities._application._activities as azure_activities +import time_tracker.activities._infrastructure as infrastructure +from time_tracker._infrastructure import DB +from time_tracker.activities import _domain + +ACTIVITY_URL = '/api/activities/' -ACTIVITY_URL = "/api/activities/" + +@pytest.fixture(name='insert_activity') +def _insert_activity() -> dict: + def _new_activity(activity: _domain.Activity, database: DB): + dao = infrastructure.ActivitiesSQLDao(database) + new_activity = dao.create(activity) + return new_activity.__dict__ + return _new_activity def test__activity_azure_endpoint__returns_all_activities( - create_temp_activities, + create_fake_database, activity_factory, insert_activity ): - activities_json, tmp_directory = create_temp_activities - activities._get_activities.JSON_PATH = tmp_directory - req = func.HttpRequest(method="GET", body=None, url=ACTIVITY_URL) - - response = activities.get_activities(req) + fake_database = create_fake_database + existent_activities = [activity_factory(), activity_factory()] + inserted_activities = [ + insert_activity(existent_activities[0], fake_database), + insert_activity(existent_activities[1], fake_database) + ] + + azure_activities._get_activities.DATABASE = fake_database + req = func.HttpRequest(method='GET', body=None, url=ACTIVITY_URL) + response = azure_activities._get_activities.get_activities(req) activities_json_data = response.get_body().decode("utf-8") assert response.status_code == 200 - assert activities_json_data == json.dumps(activities_json) + assert activities_json_data == json.dumps(inserted_activities) def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_its_id( - create_temp_activities, + create_fake_database, activity_factory, insert_activity ): - activities_json, tmp_directory = create_temp_activities - activities._get_activities.JSON_PATH = tmp_directory + fake_database = create_fake_database + existent_activity = activity_factory() + inserted_activity = insert_activity(existent_activity, fake_database) + + azure_activities._get_activities.DATABASE = fake_database req = func.HttpRequest( - method="GET", + method='GET', body=None, url=ACTIVITY_URL, - route_params={"id": activities_json[0]["id"]}, + route_params={"id": inserted_activity["id"]}, ) - response = activities.get_activities(req) + response = azure_activities._get_activities.get_activities(req) activitiy_json_data = response.get_body().decode("utf-8") assert response.status_code == 200 - assert activitiy_json_data == json.dumps(activities_json[0]) + assert activitiy_json_data == json.dumps(inserted_activity) def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__when_an_activity_matching_its_id_is_found( - create_temp_activities, + create_fake_database, activity_factory, insert_activity ): - activities_json, tmp_directory = create_temp_activities - activities._delete_activity.JSON_PATH = tmp_directory + fake_database = create_fake_database + existent_activity = activity_factory() + inserted_activity = insert_activity(existent_activity, fake_database) + + azure_activities._delete_activity.DATABASE = fake_database req = func.HttpRequest( - method="DELETE", + method='DELETE', body=None, url=ACTIVITY_URL, - route_params={"id": activities_json[0]["id"]}, + route_params={"id": inserted_activity["id"]}, ) - response = activities.delete_activity(req) + response = azure_activities._delete_activity.delete_activity(req) activity_json_data = json.loads(response.get_body().decode("utf-8")) assert response.status_code == 200 - assert activity_json_data["status"] == "inactive" + assert activity_json_data['status'] == 0 + assert activity_json_data['deleted'] is True def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_activity_to_update( - create_temp_activities, + create_fake_database, activity_factory, insert_activity ): - activities_json, tmp_directory = create_temp_activities - activities._update_activity.JSON_PATH = tmp_directory - activity_data = {"description": Faker().sentence()} + fake_database = create_fake_database + existent_activity = activity_factory() + inserted_activity = insert_activity(existent_activity, fake_database) + + azure_activities._update_activity.DATABASE = fake_database + activity_body = {"description": Faker().sentence()} req = func.HttpRequest( - method="PUT", - body=json.dumps(activity_data).encode("utf-8"), + method='PUT', + body=json.dumps(activity_body).encode("utf-8"), url=ACTIVITY_URL, - route_params={"id": activities_json[0]["id"]}, + route_params={"id": inserted_activity["id"]}, ) - response = activities.update_activity(req) + response = azure_activities._update_activity.update_activity(req) activitiy_json_data = response.get_body().decode("utf-8") - new_activity = {**activities_json[0], **activity_data} + inserted_activity.update(activity_body) assert response.status_code == 200 - assert activitiy_json_data == json.dumps(new_activity) + assert activitiy_json_data == json.dumps(inserted_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 - + create_fake_database, + ): + azure_activities._create_activity.DATABASE = create_fake_database activity_body = { - "id": None, - "name": Faker().user_name(), - "description": Faker().sentence(), - "deleted": Faker().uuid4(), - "status": "active", - "tenant_id": Faker().uuid4(), + 'id': None, + 'name': Faker().user_name(), + 'description': Faker().sentence(), + 'deleted': False, + 'status': 1 } body = json.dumps(activity_body).encode("utf-8") req = func.HttpRequest( - method="POST", - body=body, - url=ACTIVITY_URL, + method='POST', + body=body, + url=ACTIVITY_URL, ) - response = activities.create_activity(req) - activitiy_json_data = response.get_body() + response = azure_activities._create_activity.create_activity(req) + activitiy_json_data = json.loads(response.get_body()) + activity_body['id'] = activitiy_json_data['id'] + assert response.status_code == 201 - assert activitiy_json_data == body + assert activitiy_json_data == activity_body diff --git a/V2/tests/conftest.py b/V2/tests/conftest.py index 2741ce95..d1c4928f 100644 --- a/V2/tests/conftest.py +++ b/V2/tests/conftest.py @@ -1,2 +1,2 @@ # flake8: noqa -from tests.api.api_fixtures import create_temp_activities +from fixtures import _activity_factory, _create_fake_dao, _create_fake_database \ No newline at end of file diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py new file mode 100644 index 00000000..d9539035 --- /dev/null +++ b/V2/tests/fixtures.py @@ -0,0 +1,35 @@ +import pytest + +import time_tracker.activities._domain as domain +import time_tracker.activities._infrastructure as infrastructure +from time_tracker._infrastructure import DB +from faker import Faker + + +@pytest.fixture(name='activity_factory') +def _activity_factory() -> domain.Activity: + def _make_activity( + name: str = Faker().name(), description: str = Faker().sentence(), deleted: bool = False, status: int = 1 + ): + activity = domain.Activity( + id=None, + name=name, + description=description, + deleted=deleted, + status=status + ) + return activity + return _make_activity + + +@pytest.fixture(name='create_fake_dao') +def _create_fake_dao() -> domain.ActivitiesDao: + db_fake = DB('sqlite:///:memory:') + dao = infrastructure.ActivitiesSQLDao(db_fake) + return dao + + +@pytest.fixture(name='create_fake_database') +def _create_fake_database() -> domain.ActivitiesDao: + db_fake = DB('sqlite:///:memory:') + return db_fake diff --git a/V2/tests/integration/daos/activities_json_dao_test.py b/V2/tests/integration/daos/activities_json_dao_test.py deleted file mode 100644 index 8eff9609..00000000 --- a/V2/tests/integration/daos/activities_json_dao_test.py +++ /dev/null @@ -1,152 +0,0 @@ -from time_tracker.activities._infrastructure import ActivitiesJsonDao -from time_tracker.activities._domain import Activity -from faker import Faker -import json -import pytest -import typing - - -fake_activities = [ - { - "id": Faker().uuid4(), - "name": Faker().user_name(), - "description": Faker().sentence(), - "deleted": Faker().uuid4(), - "status": "active", - "tenant_id": Faker().uuid4(), - } -] - - -@pytest.fixture(name="create_fake_activities") -def _create_fake_activities(mocker) -> typing.List[Activity]: - def _creator(activities): - read_data = json.dumps(activities) - mocker.patch("builtins.open", mocker.mock_open(read_data=read_data)) - return [Activity(**activity) for activity in activities] - - return _creator - - -def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matches_its_id( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - activities = create_fake_activities(fake_activities) - activity_dto = activities.pop() - - result = activities_json_dao.get_by_id(activity_dto.id) - - assert result == activity_dto - - -def test__get_by_id__returns_none__when_no_activity_matches_its_id( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - create_fake_activities([]) - - result = activities_json_dao.get_by_id(Faker().uuid4()) - - assert result is None - - -def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_activities_are_found( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - number_of_activities = 3 - activities = create_fake_activities(fake_activities * number_of_activities) - - result = activities_json_dao.get_all() - - assert result == activities - - -def test_get_all__returns_an_empty_list__when_doesnt_found_any_activities( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - activities = create_fake_activities([]) - - result = activities_json_dao.get_all() - - assert result == activities - - -def test_delete__returns_an_activity_with_inactive_status__when_an_activity_matching_its_id_is_found( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - activities = create_fake_activities( - [ - { - "name": "test_name", - "description": "test_description", - "tenant_id": "test_tenant_id", - "id": "test_id", - "deleted": "test_deleted", - "status": "test_status", - } - ] - ) - - activity_dto = activities.pop() - result = activities_json_dao.delete(activity_dto.id) - - assert result.status == "inactive" - - -def test_delete__returns_none__when_no_activity_matching_its_id_is_found( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - create_fake_activities([]) - - result = activities_json_dao.delete(Faker().uuid4()) - - assert result is None - - -def test_update__returns_an_activity_dto__when_found_one_activity_to_update( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - activities = create_fake_activities(fake_activities) - activity_dto = activities.pop() - activity_data = {"description": Faker().sentence()} - - result = activities_json_dao.update(activity_dto.id, activity_data) - new_activity = {**activity_dto.__dict__, **activity_data} - - assert result == Activity(**new_activity) - - -def test_update__returns_none__when_doesnt_found_one_activity_to_update( - create_fake_activities, -): - activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - create_fake_activities([]) - activity_data = {"description": Faker().sentence()} - - result = activities_json_dao.update("", activity_data) - - assert result is 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) diff --git a/V2/tests/integration/daos/activities_sql_dao_test.py b/V2/tests/integration/daos/activities_sql_dao_test.py new file mode 100644 index 00000000..25f62500 --- /dev/null +++ b/V2/tests/integration/daos/activities_sql_dao_test.py @@ -0,0 +1,138 @@ +import pytest +import typing +from faker import Faker + +import time_tracker.activities._domain as domain +import time_tracker.activities._infrastructure as infrastructure +from time_tracker._infrastructure import DB + + +@pytest.fixture(name='insert_activity') +def _insert_activity() -> domain.Activity: + def _new_activity(activity: domain.Activity, dao: domain.ActivitiesDao): + new_activity = dao.create(activity) + return new_activity + return _new_activity + + +@pytest.fixture(name='clean_database', autouse=True) +def _clean_database(): + yield + db_fake = DB('sqlite:///:memory:') + dao = infrastructure.ActivitiesSQLDao(db_fake) + query = dao.activity.delete() + dao.db.get_session().execute(query) + + +def test__create_activity__returns_a_activity_dto__when_saves_correctly_with_sql_database( + create_fake_dao, activity_factory +): + dao = create_fake_dao + existent_activity = activity_factory() + + inserted_activity = dao.create(existent_activity) + + assert isinstance(inserted_activity, domain.Activity) + assert inserted_activity == existent_activity + + +def test_update__returns_an_update_activity__when_an_activity_matching_its_id_is_found_with_sql_database( + create_fake_dao, activity_factory, insert_activity +): + dao = create_fake_dao + existent_activity = activity_factory() + inserted_activity = insert_activity(existent_activity, dao) + + expected_description = Faker().sentence() + updated_activity = dao.update(inserted_activity.id, None, expected_description, None, None) + + assert isinstance(updated_activity, domain.Activity) + assert updated_activity.id == inserted_activity.id + assert updated_activity.description == expected_description + + +def test_update__returns_none__when_no_activity_matching_its_id_is_found_with_sql_database( + create_fake_dao, activity_factory +): + dao = create_fake_dao + existent_activity = activity_factory() + + results = dao.update(existent_activity.id, Faker().name(), None, None, None) + + assert results is None + + +def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_activities_are_found_with_sql_database( + create_fake_dao, activity_factory, insert_activity +): + dao = create_fake_dao + existent_activities = [activity_factory(), activity_factory()] + inserted_activities = [ + insert_activity(existent_activities[0], dao), + insert_activity(existent_activities[1], dao) + ] + + activities = dao.get_all() + + assert isinstance(activities, typing.List) + assert activities == inserted_activities + + +def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matches_its_id_with_sql_database( + create_fake_dao, activity_factory, insert_activity +): + dao = create_fake_dao + existent_activity = activity_factory() + inserted_activity = insert_activity(existent_activity, dao) + + activity = dao.get_by_id(inserted_activity.id) + + assert isinstance(activity, domain.Activity) + assert activity.id == inserted_activity.id + assert activity == inserted_activity + + +def test__get_by_id__returns_none__when_no_activity_matches_its_id_with_sql_database( + create_fake_dao, activity_factory +): + dao = create_fake_dao + existent_activity = activity_factory() + + activity = dao.get_by_id(existent_activity.id) + + assert activity is None + + +def test_get_all__returns_an_empty_list__when_doesnt_found_any_activities_with_sql_database( + create_fake_dao +): + activities = create_fake_dao.get_all() + + assert isinstance(activities, typing.List) + assert activities == [] + + +def test_delete__returns_an_activity_with_inactive_status__when_an_activity_matching_its_id_is_found_with_sql_database( + create_fake_dao, activity_factory, insert_activity +): + dao = create_fake_dao + existent_activity = activity_factory() + inserted_activity = insert_activity(existent_activity, dao) + + activity = dao.delete(inserted_activity.id) + + assert isinstance(activity, domain.Activity) + assert activity.id == inserted_activity.id + assert activity.status == 0 + assert activity.deleted is True + + +def test_delete__returns_none__when_no_activity_matching_its_id_is_found_with_sql_database( + create_fake_dao, activity_factory +): + dao = create_fake_dao + existent_activity = activity_factory() + + results = dao.delete(existent_activity.id) + + assert results is None diff --git a/V2/tests/unit/services/activity_service_test.py b/V2/tests/unit/services/activity_service_test.py index befdb1fb..e8816d42 100644 --- a/V2/tests/unit/services/activity_service_test.py +++ b/V2/tests/unit/services/activity_service_test.py @@ -53,7 +53,7 @@ def test__update_activity__uses_the_activity_dao__to_update_one_activity( activity_service = ActivityService(activity_dao) updated_activity = activity_service.update( - Faker().uuid4(), Faker().pydict() + Faker().uuid4(), Faker().name(), Faker().sentence(), Faker().pyint(), Faker().pybool() ) assert activity_dao.update.called @@ -63,11 +63,11 @@ def test__update_activity__uses_the_activity_dao__to_update_one_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) + create=mocker.Mock(return_value=expected_activity) ) activity_service = ActivityService(activity_dao) - actual_activity = activity_service.create_activity(Faker().pydict()) + actual_activity = activity_service.create(Faker().pydict()) - assert activity_dao.create_activity.called + assert activity_dao.create.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 334c7489..ca711019 100644 --- a/V2/tests/unit/use_cases/activities_use_case_test.py +++ b/V2/tests/unit/use_cases/activities_use_case_test.py @@ -1,6 +1,7 @@ -from time_tracker.activities._domain import _use_cases -from pytest_mock import MockFixture from faker import Faker +from pytest_mock import MockFixture + +from time_tracker.activities._domain import _use_cases fake = Faker() @@ -36,17 +37,17 @@ def test__get_activity_by_id_function__uses_the_activity_service__to_retrieve_ac def test__create_activity_function__uses_the_activities_service__to_create_activity( - mocker: MockFixture, -): + mocker: MockFixture, activity_factory + ): expected_activity = mocker.Mock() activity_service = mocker.Mock( - create_activity=mocker.Mock(return_value=expected_activity) + create=mocker.Mock(return_value=expected_activity) ) activity_use_case = _use_cases.CreateActivityUseCase(activity_service) - actual_activity = activity_use_case.create_activity(fake.pydict()) + actual_activity = activity_use_case.create_activity(activity_factory()) - assert activity_service.create_activity.called + assert activity_service.create.called assert expected_activity == actual_activity @@ -54,7 +55,9 @@ def test__delete_activity_function__uses_the_activity_service__to_change_activit mocker: MockFixture, ): expected_activity = mocker.Mock() - activity_service = mocker.Mock(delete=mocker.Mock(return_value=expected_activity)) + activity_service = mocker.Mock( + delete=mocker.Mock(return_value=expected_activity) + ) activity_use_case = _use_cases.DeleteActivityUseCase(activity_service) deleted_activity = activity_use_case.delete_activity(fake.uuid4()) @@ -64,13 +67,18 @@ def test__delete_activity_function__uses_the_activity_service__to_change_activit def test__update_activity_function__uses_the_activities_service__to_update_an_activity( - mocker: MockFixture, + mocker: MockFixture, activity_factory ): expected_activity = mocker.Mock() - activity_service = mocker.Mock(update=mocker.Mock(return_value=expected_activity)) + activity_service = mocker.Mock( + update=mocker.Mock(return_value=expected_activity) + ) + new_activity = activity_factory() activity_use_case = _use_cases.UpdateActivityUseCase(activity_service) - updated_activity = activity_use_case.update_activity(fake.uuid4(), fake.pydict()) + updated_activity = activity_use_case.update_activity( + fake.uuid4(), new_activity.name, new_activity.description, new_activity.status, new_activity.deleted + ) assert activity_service.update.called assert expected_activity == updated_activity diff --git a/V2/time_tracker/_infrastructure/__init__.py b/V2/time_tracker/_infrastructure/__init__.py new file mode 100644 index 00000000..ab651958 --- /dev/null +++ b/V2/time_tracker/_infrastructure/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from ._db import DB +from ._config import Config diff --git a/V2/time_tracker/_infrastructure/_config.py b/V2/time_tracker/_infrastructure/_config.py new file mode 100644 index 00000000..7f8c8fa7 --- /dev/null +++ b/V2/time_tracker/_infrastructure/_config.py @@ -0,0 +1,20 @@ +import typing +import os + +CONNECTION_STRING = 'postgresql://root:root@localhost:5433/timetracker' + + +class Config(typing.NamedTuple): + DB_CONNECTION_STRING: str + DB_USER: str + DB_PASS: str + DB_NAME: str + + +def load_config(): + return Config( + CONNECTION_STRING if os.environ.get("DB_CONNECTION_STRING") is None else os.environ.get("DB_CONNECTION_STRING"), + os.environ.get("DB_USER"), + os.environ.get("DB_PASS"), + os.environ.get("DB_NAME") + ) diff --git a/V2/time_tracker/_infrastructure/_db.py b/V2/time_tracker/_infrastructure/_db.py new file mode 100644 index 00000000..8fe5cef1 --- /dev/null +++ b/V2/time_tracker/_infrastructure/_db.py @@ -0,0 +1,20 @@ +import sqlalchemy + +from . import _config + + +class DB(): + config = _config.load_config() + connection = None + engine = None + conn_string = config.DB_CONNECTION_STRING + metadata = sqlalchemy.MetaData() + + def __init__(self, conn_string: str = conn_string): + self.engine = sqlalchemy.create_engine(conn_string) + + def get_session(self): + if self.connection is None: + self.metadata.create_all(self.engine) + self.connection = self.engine.connect() + return self.connection diff --git a/V2/time_tracker/activities/_application/_activities/_create_activity.py b/V2/time_tracker/activities/_application/_activities/_create_activity.py index be53815a..94f3701d 100644 --- a/V2/time_tracker/activities/_application/_activities/_create_activity.py +++ b/V2/time_tracker/activities/_application/_activities/_create_activity.py @@ -6,14 +6,13 @@ from ... import _domain from ... import _infrastructure +from time_tracker._infrastructure import DB -_JSON_PATH = ( - 'activities/_infrastructure/_data_persistence/activities_data.json' -) +DATABASE = DB() def create_activity(req: func.HttpRequest) -> func.HttpResponse: - activity_dao = _infrastructure.ActivitiesJsonDao(_JSON_PATH) + activity_dao = _infrastructure.ActivitiesSQLDao(DATABASE) activity_service = _domain.ActivityService(activity_dao) use_case = _domain._use_cases.CreateActivityUseCase(activity_service) @@ -30,11 +29,10 @@ def create_activity(req: func.HttpRequest) -> func.HttpResponse: name=activity_data['name'], description=activity_data['description'], status=activity_data['status'], - deleted=activity_data['deleted'], - tenant_id=activity_data['tenant_id'] + deleted=activity_data['deleted'] ) - created_activity = use_case.create_activity(activity_to_create.__dict__) + created_activity = use_case.create_activity(activity_to_create) if not create_activity: return func.HttpResponse( body={'error': 'activity could not be created'}, diff --git a/V2/time_tracker/activities/_application/_activities/_delete_activity.py b/V2/time_tracker/activities/_application/_activities/_delete_activity.py index 80d55446..14ada8ab 100644 --- a/V2/time_tracker/activities/_application/_activities/_delete_activity.py +++ b/V2/time_tracker/activities/_application/_activities/_delete_activity.py @@ -1,36 +1,41 @@ -from time_tracker.activities._infrastructure import ActivitiesJsonDao -from time_tracker.activities._domain import ActivityService, _use_cases - -import azure.functions as func import json import logging -JSON_PATH = ( - 'activities/_infrastructure/_data_persistence/activities_data.json' -) +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + +DATABASE = DB() def delete_activity(req: func.HttpRequest) -> func.HttpResponse: logging.info( 'Python HTTP trigger function processed a request to delete an activity.' ) - activity_id = req.route_params.get('id') - response = _delete(activity_id) - status_code = 200 if response != b'Not found' else 404 - - return func.HttpResponse( - body=response, status_code=status_code, mimetype="application/json" - ) - - -def _delete(activity_id: str) -> str: - activity_use_case = _use_cases.DeleteActivityUseCase( - _create_activity_service(JSON_PATH) + try: + activity_id = int(req.route_params.get('id')) + response = _delete(activity_id) + status_code = 200 if response != b'Not found' else 404 + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + except ValueError: + return func.HttpResponse( + body=b"Invalid format id", status_code=400, mimetype="application/json" + ) + + +def _delete(activity_id: int) -> str: + activity_use_case = _domain._use_cases.DeleteActivityUseCase( + _create_activity_service(DATABASE) ) activity = activity_use_case.delete_activity(activity_id) return json.dumps(activity.__dict__) if activity else b'Not found' -def _create_activity_service(path: str): - activity_json = ActivitiesJsonDao(path) - return ActivityService(activity_json) +def _create_activity_service(db: DB) -> _domain.ActivityService: + activity_sql = _infrastructure.ActivitiesSQLDao(db) + return _domain.ActivityService(activity_sql) diff --git a/V2/time_tracker/activities/_application/_activities/_get_activities.py b/V2/time_tracker/activities/_application/_activities/_get_activities.py index 9f52069d..d92503dd 100644 --- a/V2/time_tracker/activities/_application/_activities/_get_activities.py +++ b/V2/time_tracker/activities/_application/_activities/_get_activities.py @@ -1,13 +1,13 @@ -from time_tracker.activities._infrastructure import ActivitiesJsonDao -from time_tracker.activities._domain import ActivityService, _use_cases - -import azure.functions as func import json import logging -JSON_PATH = ( - 'activities/_infrastructure/_data_persistence/activities_data.json' -) +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + +DATABASE = DB() def get_activities(req: func.HttpRequest) -> func.HttpResponse: @@ -17,21 +17,26 @@ def get_activities(req: func.HttpRequest) -> func.HttpResponse: activity_id = req.route_params.get('id') status_code = 200 - if activity_id: - response = _get_by_id(activity_id) - if response == b'Not Found': - status_code = 404 - else: - response = _get_all() - - return func.HttpResponse( - body=response, status_code=status_code, mimetype="application/json" - ) - - -def _get_by_id(activity_id: str) -> str: - activity_use_case = _use_cases.GetActivityUseCase( - _create_activity_service(JSON_PATH) + try: + if activity_id: + response = _get_by_id(int(activity_id)) + if response == b'Not Found': + status_code = 404 + else: + response = _get_all() + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + except ValueError: + return func.HttpResponse( + body=b"Invalid format id", status_code=400, mimetype="application/json" + ) + + +def _get_by_id(activity_id: int) -> str: + activity_use_case = _domain._use_cases.GetActivityUseCase( + _create_activity_service(DATABASE) ) activity = activity_use_case.get_activity_by_id(activity_id) @@ -39,8 +44,8 @@ def _get_by_id(activity_id: str) -> str: def _get_all() -> str: - activities_use_case = _use_cases.GetActivitiesUseCase( - _create_activity_service(JSON_PATH) + activities_use_case = _domain._use_cases.GetActivitiesUseCase( + _create_activity_service(DATABASE) ) return json.dumps( [ @@ -50,6 +55,6 @@ def _get_all() -> str: ) -def _create_activity_service(path: str): - activity_json = ActivitiesJsonDao(path) - return ActivityService(activity_json) +def _create_activity_service(db: DB) -> _domain.ActivityService: + activity_sql = _infrastructure.ActivitiesSQLDao(db) + return _domain.ActivityService(activity_sql) diff --git a/V2/time_tracker/activities/_application/_activities/_update_activity.py b/V2/time_tracker/activities/_application/_activities/_update_activity.py index 1709f77a..0933fd72 100644 --- a/V2/time_tracker/activities/_application/_activities/_update_activity.py +++ b/V2/time_tracker/activities/_application/_activities/_update_activity.py @@ -1,44 +1,54 @@ -from time_tracker.activities._infrastructure import ActivitiesJsonDao -from time_tracker.activities._domain import ActivityService, Activity, _use_cases - -import azure.functions as func import dataclasses import json import logging -JSON_PATH = ( - 'activities/_infrastructure/_data_persistence/activities_data.json' -) +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + +DATABASE = DB() def update_activity(req: func.HttpRequest) -> func.HttpResponse: logging.info( 'Python HTTP trigger function processed a request to update an activity.' ) - activity_id = req.route_params.get('id') - activity_data = req.get_json() if req.get_body() else {} - activity_keys = [field.name for field in dataclasses.fields(Activity)] - - if all(key in activity_keys for key in activity_data.keys()): - response = _update(activity_id, activity_data) - status_code = 200 - else: - response = b'Incorrect activity body' - status_code = 400 - - return func.HttpResponse( - body=response, status_code=status_code, mimetype="application/json" - ) - - -def _update(activity_id: str, activity_data: dict) -> str: - activity_use_case = _use_cases.UpdateActivityUseCase( - _create_activity_service(JSON_PATH) + try: + activity_id = int(req.route_params.get('id')) + activity_data = req.get_json() if req.get_body() else {} + activity_keys = [field.name for field in dataclasses.fields(_domain.Activity)] + + if all(key in activity_keys for key in activity_data.keys()): + response = _update(activity_id, activity_data) + status_code = 200 + else: + response = b'Incorrect activity body' + status_code = 400 + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + except ValueError: + return func.HttpResponse( + body=b"Invalid format id", status_code=400, mimetype="application/json" + ) + + +def _update(activity_id: int, activity_data: dict) -> str: + activity_use_case = _domain._use_cases.UpdateActivityUseCase( + _create_activity_service(DATABASE) ) - activity = activity_use_case.update_activity(activity_id, activity_data) + activity = activity_use_case.update_activity( + activity_id, activity_data.get("name"), + activity_data.get("description"), + activity_data.get("status"), + activity_data.get("deleted") + ) return json.dumps(activity.__dict__) if activity else b'Not Found' -def _create_activity_service(path: str): - activity_json = ActivitiesJsonDao(path) - return ActivityService(activity_json) +def _create_activity_service(db: DB) -> _domain.ActivityService: + activity_sql = _infrastructure.ActivitiesSQLDao(db) + return _domain.ActivityService(activity_sql) diff --git a/V2/time_tracker/activities/_domain/_entities/_activity.py b/V2/time_tracker/activities/_domain/_entities/_activity.py index 86f56ee9..cf574054 100644 --- a/V2/time_tracker/activities/_domain/_entities/_activity.py +++ b/V2/time_tracker/activities/_domain/_entities/_activity.py @@ -3,9 +3,8 @@ @dataclass(frozen=True) class Activity: - id: str + id: int name: str description: str - deleted: str - status: str - tenant_id: str + deleted: bool + status: int diff --git a/V2/time_tracker/activities/_domain/_persistence_contracts/_activities_dao.py b/V2/time_tracker/activities/_domain/_persistence_contracts/_activities_dao.py index 80b8c711..e079ed6a 100644 --- a/V2/time_tracker/activities/_domain/_persistence_contracts/_activities_dao.py +++ b/V2/time_tracker/activities/_domain/_persistence_contracts/_activities_dao.py @@ -5,7 +5,7 @@ class ActivitiesDao(abc.ABC): @abc.abstractmethod - def get_by_id(self, id: str) -> Activity: + def get_by_id(self, id: int) -> Activity: pass @abc.abstractmethod @@ -13,13 +13,13 @@ def get_all(self) -> typing.List[Activity]: pass @abc.abstractmethod - def delete(self, id: str) -> Activity: + def delete(self, id: int) -> Activity: pass @abc.abstractmethod - def update(self, id: str, new_activity: dict) -> Activity: + def update(self, id: int, name: str, description: str, status: int, deleted: bool) -> Activity: pass @abc.abstractmethod - def create_activity(self, activity_data: dict) -> Activity: + def create(self, activity_data: Activity) -> Activity: pass diff --git a/V2/time_tracker/activities/_domain/_services/_activity.py b/V2/time_tracker/activities/_domain/_services/_activity.py index a564577a..a2c45e54 100644 --- a/V2/time_tracker/activities/_domain/_services/_activity.py +++ b/V2/time_tracker/activities/_domain/_services/_activity.py @@ -6,17 +6,17 @@ class ActivityService: def __init__(self, activities_dao: ActivitiesDao): self.activities_dao = activities_dao - def get_by_id(self, activity_id: str) -> Activity: + def get_by_id(self, activity_id: int) -> Activity: return self.activities_dao.get_by_id(activity_id) def get_all(self) -> typing.List[Activity]: return self.activities_dao.get_all() - def delete(self, activity_id: str) -> Activity: + def delete(self, activity_id: int) -> Activity: return self.activities_dao.delete(activity_id) - def update(self, activity_id: str, new_activity: dict) -> Activity: - return self.activities_dao.update(activity_id, new_activity) + def update(self, activity_id: int, name: str, description: str, status: int, deleted: bool) -> Activity: + return self.activities_dao.update(activity_id, name, description, status, deleted) - def create_activity(self, activity_data: dict) -> Activity: - return self.activities_dao.create_activity(activity_data) + def create(self, activity_data: Activity) -> Activity: + return self.activities_dao.create(activity_data) diff --git a/V2/time_tracker/activities/_domain/_use_cases/_create_activity_use_case.py b/V2/time_tracker/activities/_domain/_use_cases/_create_activity_use_case.py index 241718db..26d0f475 100644 --- a/V2/time_tracker/activities/_domain/_use_cases/_create_activity_use_case.py +++ b/V2/time_tracker/activities/_domain/_use_cases/_create_activity_use_case.py @@ -5,5 +5,5 @@ 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) + def create_activity(self, activity_data: Activity) -> Activity: + return self.activity_service.create(activity_data) diff --git a/V2/time_tracker/activities/_domain/_use_cases/_delete_activity_use_case.py b/V2/time_tracker/activities/_domain/_use_cases/_delete_activity_use_case.py index 5af54ee8..67fcf31c 100644 --- a/V2/time_tracker/activities/_domain/_use_cases/_delete_activity_use_case.py +++ b/V2/time_tracker/activities/_domain/_use_cases/_delete_activity_use_case.py @@ -5,5 +5,5 @@ class DeleteActivityUseCase: def __init__(self, activity_service: ActivityService): self.activity_service = activity_service - def delete_activity(self, id: str) -> Activity: + def delete_activity(self, id: int) -> Activity: return self.activity_service.delete(id) diff --git a/V2/time_tracker/activities/_domain/_use_cases/_get_activity_by_id_use_case.py b/V2/time_tracker/activities/_domain/_use_cases/_get_activity_by_id_use_case.py index 04ca442e..45dbbad0 100644 --- a/V2/time_tracker/activities/_domain/_use_cases/_get_activity_by_id_use_case.py +++ b/V2/time_tracker/activities/_domain/_use_cases/_get_activity_by_id_use_case.py @@ -5,5 +5,5 @@ class GetActivityUseCase: def __init__(self, activity_service: ActivityService): self.activity_service = activity_service - def get_activity_by_id(self, id: str) -> Activity: + def get_activity_by_id(self, id: int) -> Activity: return self.activity_service.get_by_id(id) diff --git a/V2/time_tracker/activities/_domain/_use_cases/_update_activity_use_case.py b/V2/time_tracker/activities/_domain/_use_cases/_update_activity_use_case.py index a890d85f..c270f465 100644 --- a/V2/time_tracker/activities/_domain/_use_cases/_update_activity_use_case.py +++ b/V2/time_tracker/activities/_domain/_use_cases/_update_activity_use_case.py @@ -6,6 +6,6 @@ def __init__(self, activity_service: ActivityService): self.activity_service = activity_service def update_activity( - self, activity_id: str, new_activity: dict + self, activity_id: int, name: str, description: str, status: int, deleted: bool ) -> Activity: - return self.activity_service.update(activity_id, new_activity) + return self.activity_service.update(activity_id, name, description, status, deleted) diff --git a/V2/time_tracker/activities/_infrastructure/__init__.py b/V2/time_tracker/activities/_infrastructure/__init__.py index 1734e5b8..b3896baf 100644 --- a/V2/time_tracker/activities/_infrastructure/__init__.py +++ b/V2/time_tracker/activities/_infrastructure/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from ._data_persistence import ActivitiesJsonDao +from ._data_persistence import ActivitiesSQLDao diff --git a/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py b/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py index d2a77fc4..1e7220c5 100644 --- a/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py +++ b/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from ._activities_json_dao import ActivitiesJsonDao +from ._activities_sql_dao import ActivitiesSQLDao diff --git a/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_json_dao.py b/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_json_dao.py deleted file mode 100644 index 60859a15..00000000 --- a/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_json_dao.py +++ /dev/null @@ -1,105 +0,0 @@ -from time_tracker.activities._domain import ActivitiesDao, Activity -import dataclasses -import json -import typing - - -class ActivitiesJsonDao(ActivitiesDao): - def __init__(self, json_data_file_path: str): - self.json_data_file_path = json_data_file_path - self.activity_keys = [ - field.name for field in dataclasses.fields(Activity) - ] - - def get_by_id(self, activity_id: str) -> Activity: - activity = { - activity.get('id'): activity - for activity in self.__get_activities_from_file() - }.get(activity_id) - - return self.__create_activity_dto(activity) if activity else None - - def get_all(self) -> typing.List[Activity]: - return [ - self.__create_activity_dto(activity) - for activity in self.__get_activities_from_file() - ] - - def delete(self, activity_id: str) -> Activity: - activity = self.get_by_id(activity_id) - if activity: - activity_deleted = {**activity.__dict__, 'status': 'inactive'} - activities_updated = list( - map( - lambda activity: activity - if activity.get('id') != activity_id - else activity_deleted, - self.__get_activities_from_file(), - ) - ) - - try: - file = open(self.json_data_file_path, 'w') - json.dump(activities_updated, file) - file.close() - - return self.__create_activity_dto(activity_deleted) - - except FileNotFoundError: - return None - - else: - return None - - def update(self, activity_id: str, new_activity: dict) -> Activity: - activity = self.get_by_id(activity_id) - if not activity: - return None - - new_activity = {**activity.__dict__, **new_activity} - - activities_updated = list( - map( - lambda activity: activity - if activity.get('id') != activity_id - else new_activity, - self.__get_activities_from_file(), - ) - ) - - try: - file = open(self.json_data_file_path, 'w') - json.dump(activities_updated, file) - file.close() - - return self.__create_activity_dto(new_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) - activities = json.load(file) - file.close() - - return activities - - except FileNotFoundError: - return [] - - def __create_activity_dto(self, activity: dict) -> Activity: - activity = {key: activity.get(key) for key in self.activity_keys} - return Activity(**activity) diff --git a/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py b/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py new file mode 100644 index 00000000..e69dd1a4 --- /dev/null +++ b/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py @@ -0,0 +1,67 @@ +import dataclasses +import typing + +import sqlalchemy +import sqlalchemy.sql as sql + +import time_tracker.activities._domain as domain +from time_tracker._infrastructure import _db + + +class ActivitiesSQLDao(domain.ActivitiesDao): + + def __init__(self, database: _db.DB): + self.activity_keys = [ + field.name for field in dataclasses.fields(domain.Activity) + ] + self.db = database + self.activity = sqlalchemy.Table( + 'activity', + self.db.metadata, + sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True, autoincrement=True), + sqlalchemy.Column('name', sqlalchemy.String), + sqlalchemy.Column('description', sqlalchemy.String), + sqlalchemy.Column('deleted', sqlalchemy.Boolean), + sqlalchemy.Column('status', sqlalchemy.SmallInteger), + extend_existing=True, + ) + + def get_by_id(self, activity_id: int) -> domain.Activity: + query = sql.select(self.activity).where(self.activity.c.id == activity_id) + activity = self.db.get_session().execute(query).one_or_none() + return self.__create_activity_dto(dict(activity)) if activity else None + + def get_all(self) -> typing.List[domain.Activity]: + query = sql.select(self.activity) + result = self.db.get_session().execute(query) + return [ + self.__create_activity_dto(dict(activity)) + for activity in result + ] + + def create(self, activity_data: domain.Activity) -> domain.Activity: + new_activity = activity_data.__dict__ + new_activity.pop('id', None) + new_activity.update({"status": 1, "deleted": False}) + + query = self.activity.insert().values(new_activity).return_defaults() + activity = self.db.get_session().execute(query) + new_activity.update({"id": activity.inserted_primary_key[0]}) + return self.__create_activity_dto(new_activity) + + def delete(self, activity_id: int) -> domain.Activity: + query = self.activity.update().where(self.activity.c.id == activity_id).values({"status": 0, "deleted": True}) + self.db.get_session().execute(query) + return self.get_by_id(activity_id) + + def update(self, activity_id: int, name: str, description: str, status: int, deleted: bool) -> domain.Activity: + new_activity = {"name": name, "description": description, "status": status, "deleted": deleted} + activity_validated = {key: value for (key, value) in new_activity.items() if value is not None} + + query = self.activity.update().where(self.activity.c.id == activity_id).values(activity_validated) + self.db.get_session().execute(query) + return self.get_by_id(activity_id) + + def __create_activity_dto(self, activity: dict) -> domain.Activity: + activity = {key: activity.get(key)for key in self.activity_keys} + return domain.Activity(**activity) diff --git a/V2/time_tracker/activities/_infrastructure/_data_persistence/activities_data.json b/V2/time_tracker/activities/_infrastructure/_data_persistence/activities_data.json deleted file mode 100644 index 961251db..00000000 --- a/V2/time_tracker/activities/_infrastructure/_data_persistence/activities_data.json +++ /dev/null @@ -1,65 +0,0 @@ -[ - { - "name": "Development", - "description": "Development", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072b", - "_rid": "QUwFAPuumiRhAAAAAAAAAA==", - "_self": "dbs/QUwFAA==/colls/QUwFAPuumiQ=/docs/QUwFAPuumiRhAAAAAAAAAA==/", - "_etag": "\"4e006cc9-0000-0500-0000-607dcc0d0000\"", - "_attachments": "attachments/", - "_last_event_ctx": { - "user_id": "dd76e5d6-3949-46fd-b418-f15bf7c354fa", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - "action": "delete", - "description": null, - "container_id": "activity", - "session_id": null - }, - "deleted": "b4327ba6-9f96-49ee-a9ac-3c1edf525172", - "status": null, - "_ts": 1618856973 - }, - { - "name": "Management", - "description": null, - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - "id": "94ec92e2-a500-4700-a9f6-e41eb7b5507c", - "_last_event_ctx": { - "user_id": "dd76e5d6-3949-46fd-b418-f15bf7c354fa", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - "action": "delete", - "description": null, - "container_id": "activity", - "session_id": null - }, - "_rid": "QUwFAPuumiRfAAAAAAAAAA==", - "_self": "dbs/QUwFAA==/colls/QUwFAPuumiQ=/docs/QUwFAPuumiRfAAAAAAAAAA==/", - "_etag": "\"4e0069c9-0000-0500-0000-607dcc0d0000\"", - "_attachments": "attachments/", - "deleted": "7cf6efe5-a221-4fe4-b94f-8945127a489a", - "status": null, - "_ts": 1618856973 - }, - { - "name": "Operations", - "description": "Operation activities performed.", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - "id": "d45c770a-b1a0-4bd8-a713-22c01a23e41b", - "_rid": "QUwFAPuumiRjAAAAAAAAAA==", - "_self": "dbs/QUwFAA==/colls/QUwFAPuumiQ=/docs/QUwFAPuumiRjAAAAAAAAAA==/", - "_etag": "\"09009a4d-0000-0500-0000-614b66fb0000\"", - "_attachments": "attachments/", - "_last_event_ctx": { - "user_id": "82ed0f65-051c-4898-890f-870805900e21", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - "action": "update", - "description": null, - "container_id": "activity", - "session_id": null - }, - "deleted": "7cf6efe5-a221-4fe4-b94f-8945127a489a", - "status": "active", - "_ts": 1632331515 - } -] diff --git a/V2/time_tracker/activities/interface.py b/V2/time_tracker/activities/interface.py index 877b631e..24c888ad 100644 --- a/V2/time_tracker/activities/interface.py +++ b/V2/time_tracker/activities/interface.py @@ -2,4 +2,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 +from ._application import create_activity diff --git a/V2/update_activity/function.json b/V2/update_activity/function.json new file mode 100644 index 00000000..97c9fb49 --- /dev/null +++ b/V2/update_activity/function.json @@ -0,0 +1,22 @@ +{ + "disabled": false, + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "activities/{id}", + "authLevel": "anonymous", + "methods": [ + "PUT" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ], + "entryPoint": "update_activity", + "scriptFile": "../time_tracker/activities/interface.py" +} \ No newline at end of file From d4098cc6d82d4fdb4a0199afa738b17d9fc73df2 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Wed, 10 Nov 2021 16:53:01 -0500 Subject: [PATCH 09/18] feat: TT-404 get time entries --- .../time_entries/_application/__init__.py | 1 + .../_application/_time_entries/__init__.py | 1 + .../_time_entries/_create_time_entry.py | 49 ++++++++++ .../_time_entries/_get_time_entries.py | 55 +++++++++++ .../time_entries/_domain/__init__.py | 6 ++ .../_domain/_entities/__init__.py | 1 + .../_domain/_entities/_time_entry.py | 16 ++++ .../_persistence_contracts/__init__.py | 1 + .../_time_entries_dao.py | 16 ++++ .../_domain/_services/__init__.py | 1 + .../_domain/_services/_time_entry.py | 16 ++++ .../_domain/_use_cases/__init__.py | 3 + .../_use_cases/_create_time_entry_use_case.py | 9 ++ .../_get_time_entry_by_id_use_case.py | 9 ++ .../_use_cases/_get_time_entry_use_case.py | 9 ++ .../time_entries/_infrastructure/__init__.py | 1 + .../_data_persistence/__init__.py | 1 + .../_data_persistence/_time_entries_dao.py | 38 ++++++++ .../_data_persistence/time_entries_data.json | 92 +++++++++++++++++++ V2/time_tracker/time_entries/interface.py | 1 + 20 files changed, 326 insertions(+) create mode 100644 V2/time_tracker/time_entries/_application/__init__.py create mode 100644 V2/time_tracker/time_entries/_application/_time_entries/__init__.py create mode 100644 V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py create mode 100644 V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py create mode 100644 V2/time_tracker/time_entries/_domain/__init__.py create mode 100644 V2/time_tracker/time_entries/_domain/_entities/__init__.py create mode 100644 V2/time_tracker/time_entries/_domain/_entities/_time_entry.py create mode 100644 V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py create mode 100644 V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py create mode 100644 V2/time_tracker/time_entries/_domain/_services/__init__.py create mode 100644 V2/time_tracker/time_entries/_domain/_services/_time_entry.py create mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/__init__.py create mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py create mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py create mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py create mode 100644 V2/time_tracker/time_entries/_infrastructure/__init__.py create mode 100644 V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py create mode 100644 V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py create mode 100644 V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json create mode 100644 V2/time_tracker/time_entries/interface.py diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py new file mode 100644 index 00000000..ff94135f --- /dev/null +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -0,0 +1 @@ +from ._time_entries import create_time_entry \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py new file mode 100644 index 00000000..6d6f97f1 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -0,0 +1 @@ +from ._create_time_entry import create_time_entry \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py new file mode 100644 index 00000000..e203f16f --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py @@ -0,0 +1,49 @@ +import dataclasses +import json +import typing + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure + +_JSON_PATH = ( + 'time_entries/_infrastructure/_data_persistence/time_entries_data.json' +) + +def create_time_entry(req: func.HttpRequest) -> func.HttpResponse: + + time_entry_dao = _infrastructure.TimeEntriesJsonDao(_JSON_PATH) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.CreateTimeEntryUseCase(time_entry_service) + + time_entry_data = req.get_json() + + time_entry_to_create = _domain.TimeEntry( + id=None, + start_date=time_entry_data["start_date"], + owner_id=time_entry_data["owner_id"], + description=time_entry_data["description"], + activity_id=time_entry_data["activity_id"], + uri=time_entry_data["uri"], + technologies=time_entry_data["technologies"], + end_date=time_entry_data["end_date"], + deleted=time_entry_data["deleted"], + timezone_offset=time_entry_data["timezone_offset"], + project_id=time_entry_data["project_id"] + ) + + created_time_entry = use_case.create_time_entry(time_entry_to_create.__dict__) + + if not created_time_entry: + return func.HttpResponse( + body=json.dumps({'error': 'time_entry could not be created'}), + status_code=500, + mimetype="application/json" + ) + + return func.HttpResponse( + body=json.dumps(created_time_entry.__dict__), + status_code=201, + mimetype="application/json" + ) \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py new file mode 100644 index 00000000..34e26296 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -0,0 +1,55 @@ +from time_entries._infrastructure import TimeEntriesJsonDao +from time_entries._domain import TimeEntryService, _use_cases + +import azure.functions as func +import json +import logging + +JSON_PATH = ( + 'time_entries/_infrastructure/_data_persistence/time_entries_data.json' +) + +def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: + logging.info( + 'Python HTTP trigger function processed a request to get an activity.' + ) + # WIP + time_entry_id = req.route_params.get('id') + status_code = 200 + + if activity_id: + response = _get_by_id(activity_id) + if response == b'Not Found': + status_code = 404 + else: + response = _get_all() + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + + +def _get_by_id(activity_id: str) -> str: + activity_use_case = _use_cases.GetActivityUseCase( + _create_activity_service(JSON_PATH) + ) + activity = activity_use_case.get_activity_by_id(activity_id) + + return json.dumps(activity.__dict__) if activity else b'Not Found' + + +def _get_all() -> str: + activities_use_case = _use_cases.GetActivitiesUseCase( + _create_activity_service(JSON_PATH) + ) + return json.dumps( + [ + activity.__dict__ + for activity in activities_use_case.get_activities() + ] + ) + + +def _create_activity_service(path: str): + activity_json = ActivitiesJsonDao(path) + return ActivityService(activity_json) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py new file mode 100644 index 00000000..8d430493 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -0,0 +1,6 @@ +from ._entities import TimeEntry +from ._persistence_contracts import TimeEntriesDao +from ._services import TimeEntryService +from ._use_cases import ( + CreateTimeEntryUseCase, +) \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_entities/__init__.py b/V2/time_tracker/time_entries/_domain/_entities/__init__.py new file mode 100644 index 00000000..da523368 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_entities/__init__.py @@ -0,0 +1 @@ +from ._time_entry import TimeEntry \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py new file mode 100644 index 00000000..f5c3c573 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import List + +@dataclass(frozen=True) +class TimeEntry: + id: int + start_date: str + owner_id: int + description: str + activity_id: int + uri: str + technologies: List[str] + end_date: str + deleted: str + timezone_offset: str + project_id: int \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py new file mode 100644 index 00000000..ccf8230e --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py @@ -0,0 +1 @@ +from ._time_entries_dao import TimeEntriesDao \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py new file mode 100644 index 00000000..741ae9b3 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -0,0 +1,16 @@ +import abc +import typing + +from time_entries._domain import TimeEntry + +class TimeEntriesDao(abc.ABC): + @abc.abstractmethod + def get_by_id(self, id: str) -> TimeEntry: + pass + + @abc.abstractmethod + def get_all(self) -> typing.List[TimeEntry]: + pass + + def create(self, time_entry_data: dict) -> TimeEntry: + pass \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_services/__init__.py b/V2/time_tracker/time_entries/_domain/_services/__init__.py new file mode 100644 index 00000000..5eba7f77 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_services/__init__.py @@ -0,0 +1 @@ +from ._time_entry import TimeEntryService \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py new file mode 100644 index 00000000..a2cbb248 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -0,0 +1,16 @@ +from time_entries._domain import TimeEntry, TimeEntriesDao +import typing +class TimeEntryService: + + def __init__(self, time_entry_dao: TimeEntriesDao): + self.time_entry_dao = time_entry_dao + + + def get_by_id(self, id: str) -> TimeEntry: + return self.time_entry_dao.get_by_id(id) + + def get_all(self) -> typing.List[TimeEntry]: + return self.time_entry_dao.get_all() + + def create(self, time_entry_data: dict) -> TimeEntry: + return self.time_entry_dao.create(time_entry_data) \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py new file mode 100644 index 00000000..e2d51f8e --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -0,0 +1,3 @@ +from ._create_time_entry_use_case import CreateTimeEntryUseCase +from ._get_time_entry_use_case import GetTimeEntriesUseCase +from ._get_time_entry_by_id_use_case import GetTimeEntryUseCase \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py new file mode 100644 index 00000000..7608ff59 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py @@ -0,0 +1,9 @@ +from time_entries._domain import TimeEntry, TimeEntryService + +class CreateTimeEntryUseCase: + + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def create_time_entry(self, time_entry_data: dict) -> TimeEntry: + return self.time_entry_service.create(time_entry_data) \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py new file mode 100644 index 00000000..b5083d34 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py @@ -0,0 +1,9 @@ +from time_entries._domain import TimeEntryService, TimeEntry + + +class GetTimeEntryUseCase: + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def get_time_entry_by_id(self, id: str) -> TimeEntry: + return self.time_entry_service.get_by_id(id) diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py new file mode 100644 index 00000000..1e801773 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py @@ -0,0 +1,9 @@ +from time_entries._domain import TimeEntryService, TimeEntry +import typing + +class GetTimeEntriesUseCase: + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def get_time_entries(self) -> typing.List[TimeEntry]: + return self.time_entry_service.get_all() diff --git a/V2/time_tracker/time_entries/_infrastructure/__init__.py b/V2/time_tracker/time_entries/_infrastructure/__init__.py new file mode 100644 index 00000000..89ae5c03 --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/__init__.py @@ -0,0 +1 @@ +from ._data_persistence import TimeEntriesJsonDao \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py new file mode 100644 index 00000000..d0db4d15 --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py @@ -0,0 +1 @@ +from ._time_entries_dao import TimeEntriesJsonDao \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py new file mode 100644 index 00000000..4329d99e --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -0,0 +1,38 @@ +import json +import dataclasses +import typing + +from time_entries._domain import TimeEntriesDao, TimeEntry + +class TimeEntriesJsonDao(TimeEntriesDao): + + def __init__(self, json_data_file_path: str): + self.json_data_file_path = json_data_file_path + self.time_entry_key = [field.name for field in dataclasses.fields(TimeEntry)] + + def create(self, time_entry_data: dict) -> TimeEntry: + time_entries = self.__get_time_entries_from_file() + time_entries.append(time_entry_data) + + try: + with open(self.json_data_file_path, 'w') as outfile: + json.dump(time_entries, outfile) + + return self.__create_time_entry_dto(time_entry_data) + except FileNotFoundError: + print("Can not create activity") + + def __get_time_entries_from_file(self) -> typing.List[dict]: + try: + file = open(self.json_data_file_path) + time_entries = json.load(file) + file.close() + + return time_entries + + except FileNotFoundError: + return [] + + def __create_time_entry_dto(self, time_entry: dict) -> TimeEntry: + time_entry = {key: time_entry.get(key) for key in self.time_entry_key} + return TimeEntry(**time_entry) \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json new file mode 100644 index 00000000..2bc98c92 --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json @@ -0,0 +1,92 @@ +[{ + "id": null, + "start_date": "12/07/2021", + "owner_id": 2, + "tenant_id": "asdasdsa", + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 +}, { + "id": null, + "start_date": "12/07/2021", + "owner_id": 2, + "tenant_id": "asdasdsa", + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 +}, { + "id": null, + "start_date": "12/07/2021", + "owner_id": 2, + "tenant_id": "asdasdsa", + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 +}, { + "id": null, + "start_date": "12/07/2021", + "owner_id": 2, + "tenant_id": "asdasdsa", + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 +}, { + "id": null, + "start_date": "12/07/2021", + "owner_id": 2, + "tenant_id": "asdasdsa", + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 +}, { + "id": null, + "start_date": "12/07/2021", + "owner_id": 2, + "tenant_id": "asdasdsa", + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 +}, { + "id": null, + "start_date": "12/07/2021", + "owner_id": 2, + "tenant_id": "asdasdsa", + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 +}] \ No newline at end of file diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py new file mode 100644 index 00000000..94349dcc --- /dev/null +++ b/V2/time_tracker/time_entries/interface.py @@ -0,0 +1 @@ +from ._application import create_time_entry \ No newline at end of file From b6aa05c205b6abe4817917004f84268cf781a2ac Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Wed, 10 Nov 2021 17:09:57 -0500 Subject: [PATCH 10/18] feat: TT-404 add method GET Time entries in the file seveless.yml --- V2/serverless.yml | 12 ++++++++ .../time_entries/_application/__init__.py | 2 +- .../_application/_time_entries/__init__.py | 3 +- .../_time_entries/_get_time_entries.py | 28 +++++++++---------- V2/time_tracker/time_entries/interface.py | 3 +- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/V2/serverless.yml b/V2/serverless.yml index 0eb3f42f..b2eebebc 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -76,3 +76,15 @@ functions: - POST route: activities/ authLevel: anonymous + + get_activities: + handler: time_tracker/time_entries/interface.get_time_entries + events: + - http: true + x-azure-settings: + methods: + - GET + route: activities/{id:?} + authLevel: anonymous + + diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index ff94135f..1e8344b3 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -1 +1 @@ -from ._time_entries import create_time_entry \ No newline at end of file +from ._time_entries import create_time_entry, get_time_entries \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py index 6d6f97f1..3d9e7cf8 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -1 +1,2 @@ -from ._create_time_entry import create_time_entry \ No newline at end of file +from ._create_time_entry import create_time_entry +from ._get_time_entries import get_time_entries \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py index 34e26296..b99718b6 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -17,8 +17,8 @@ def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: time_entry_id = req.route_params.get('id') status_code = 200 - if activity_id: - response = _get_by_id(activity_id) + if time_entry_id: + response = _get_by_id(time_entry_id) if response == b'Not Found': status_code = 404 else: @@ -29,27 +29,27 @@ def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: ) -def _get_by_id(activity_id: str) -> str: - activity_use_case = _use_cases.GetActivityUseCase( - _create_activity_service(JSON_PATH) +def _get_by_id(id: str) -> str: + time_entry_use_case = _use_cases.GetTimeEntryUseCase( + _create_time_entry_service(JSON_PATH) ) - activity = activity_use_case.get_activity_by_id(activity_id) + time_entry = time_entry_use_case.get_time_entry_by_id(id) - return json.dumps(activity.__dict__) if activity else b'Not Found' + return json.dumps(time_entry.__dict__) if time_entry else b'Not Found' def _get_all() -> str: - activities_use_case = _use_cases.GetActivitiesUseCase( - _create_activity_service(JSON_PATH) + time_entries_use_case = _use_cases.GetTimeEntriesUseCase( + _create_time_entry_service(JSON_PATH) ) return json.dumps( [ - activity.__dict__ - for activity in activities_use_case.get_activities() + time_entry.__dict__ + for time_entry in time_entries_use_case.get_time_entries() ] ) -def _create_activity_service(path: str): - activity_json = ActivitiesJsonDao(path) - return ActivityService(activity_json) +def _create_time_entry_service(path: str): + activity_json = TimeEntriesJsonDao(path) + return TimeEntryService(activity_json) diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index 94349dcc..c02084ba 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -1 +1,2 @@ -from ._application import create_time_entry \ No newline at end of file +from ._application import create_time_entry +from ._application import get_time_entries From b41d8e6b8ef08049b8436e8f67a6772b8ee3ba34 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Wed, 10 Nov 2021 17:48:44 -0500 Subject: [PATCH 11/18] feat: TT-404 add method get_by_id and get all in _data_persistence --- V2/serverless.yml | 4 +- .../time_entries/_application/__init__.py | 3 +- .../_time_entries/_get_time_entries.py | 10 ++--- .../time_entries/_domain/__init__.py | 2 + .../_time_entries_dao.py | 1 + .../_data_persistence/_time_entries_dao.py | 40 +++++++++++++------ 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/V2/serverless.yml b/V2/serverless.yml index b2eebebc..d8f2b3d5 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -77,14 +77,14 @@ functions: route: activities/ authLevel: anonymous - get_activities: + get_time_entries: handler: time_tracker/time_entries/interface.get_time_entries events: - http: true x-azure-settings: methods: - GET - route: activities/{id:?} + route: time_entries/{id:?} authLevel: anonymous diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index 1e8344b3..8516ef71 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -1 +1,2 @@ -from ._time_entries import create_time_entry, get_time_entries \ No newline at end of file +from ._time_entries import create_time_entry +from ._time_entries import get_time_entries \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py index b99718b6..e4f7638d 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -3,17 +3,13 @@ import azure.functions as func import json -import logging JSON_PATH = ( 'time_entries/_infrastructure/_data_persistence/time_entries_data.json' ) def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: - logging.info( - 'Python HTTP trigger function processed a request to get an activity.' - ) - # WIP + time_entry_id = req.route_params.get('id') status_code = 200 @@ -51,5 +47,5 @@ def _get_all() -> str: def _create_time_entry_service(path: str): - activity_json = TimeEntriesJsonDao(path) - return TimeEntryService(activity_json) + time_entry_json = TimeEntriesJsonDao(path) + return TimeEntryService(time_entry_json) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index 8d430493..78c5ad3b 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -3,4 +3,6 @@ from ._services import TimeEntryService from ._use_cases import ( CreateTimeEntryUseCase, + GetTimeEntriesUseCase, + GetTimeEntryUseCase ) \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py index 741ae9b3..05bff8b4 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -12,5 +12,6 @@ def get_by_id(self, id: str) -> TimeEntry: def get_all(self) -> typing.List[TimeEntry]: pass + @abc.abstractmethod def create(self, time_entry_data: dict) -> TimeEntry: pass \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py index 4329d99e..dfeddb26 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -6,11 +6,25 @@ class TimeEntriesJsonDao(TimeEntriesDao): - def __init__(self, json_data_file_path: str): - self.json_data_file_path = json_data_file_path - self.time_entry_key = [field.name for field in dataclasses.fields(TimeEntry)] + def __init__(self, json_data_file_path: str): + self.json_data_file_path = json_data_file_path + self.time_entry_key = [field.name for field in dataclasses.fields(TimeEntry)] - def create(self, time_entry_data: dict) -> TimeEntry: + def get_by_id(self, id: str) -> TimeEntry: + time_entry = { + time_entry.get('id'): time_entry + for time_entry in self.__get_time_entries_from_file() + }.get(id) + + return self.__create_time_entry_dto(time_entry) if time_entry else None + + def get_all(self) -> typing.List[TimeEntry]: + return [ + self.__create_time_entry_dto(activity) + for activity in self.__get_time_entries_from_file() + ] + + def create(self, time_entry_data: dict) -> TimeEntry: time_entries = self.__get_time_entries_from_file() time_entries.append(time_entry_data) @@ -22,17 +36,17 @@ def create(self, time_entry_data: dict) -> TimeEntry: except FileNotFoundError: print("Can not create activity") - def __get_time_entries_from_file(self) -> typing.List[dict]: - try: - file = open(self.json_data_file_path) - time_entries = json.load(file) - file.close() + def __get_time_entries_from_file(self) -> typing.List[dict]: + try: + file = open(self.json_data_file_path) + time_entries = json.load(file) + file.close() - return time_entries + return time_entries - except FileNotFoundError: - return [] + except FileNotFoundError: + return [] - def __create_time_entry_dto(self, time_entry: dict) -> TimeEntry: + def __create_time_entry_dto(self, time_entry: dict) -> TimeEntry: time_entry = {key: time_entry.get(key) for key in self.time_entry_key} return TimeEntry(**time_entry) \ No newline at end of file From d3abc38fc4d8f32fdd78cdce57119ceaa7e43143 Mon Sep 17 00:00:00 2001 From: Jobzi Date: Wed, 10 Nov 2021 19:31:48 -0500 Subject: [PATCH 12/18] refactor: TT-404 add import prefix time_tracker --- .../_application/_time_entries/_get_time_entries.py | 6 +++--- .../_domain/_persistence_contracts/_time_entries_dao.py | 2 +- .../time_entries/_domain/_services/_time_entry.py | 2 +- .../_domain/_use_cases/_create_time_entry_use_case.py | 2 +- .../_domain/_use_cases/_get_time_entry_by_id_use_case.py | 2 +- .../_domain/_use_cases/_get_time_entry_use_case.py | 2 +- .../_infrastructure/_data_persistence/_time_entries_dao.py | 2 +- V2/time_tracker/time_entries/interface.py | 1 + 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py index e4f7638d..1ee18581 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -1,11 +1,11 @@ -from time_entries._infrastructure import TimeEntriesJsonDao -from time_entries._domain import TimeEntryService, _use_cases +from time_tracker.time_entries._infrastructure import TimeEntriesJsonDao +from time_tracker.time_entries._domain import TimeEntryService, _use_cases import azure.functions as func import json JSON_PATH = ( - 'time_entries/_infrastructure/_data_persistence/time_entries_data.json' + 'time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json' ) def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py index 05bff8b4..a4688f1a 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -1,7 +1,7 @@ import abc import typing -from time_entries._domain import TimeEntry +from time_tracker.time_entries._domain import TimeEntry class TimeEntriesDao(abc.ABC): @abc.abstractmethod diff --git a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py index a2cbb248..5eb69d88 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -1,4 +1,4 @@ -from time_entries._domain import TimeEntry, TimeEntriesDao +from time_tracker.time_entries._domain import TimeEntry, TimeEntriesDao import typing class TimeEntryService: diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py index 7608ff59..1df7be91 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py @@ -1,4 +1,4 @@ -from time_entries._domain import TimeEntry, TimeEntryService +from time_tracker.time_entries._domain import TimeEntry, TimeEntryService class CreateTimeEntryUseCase: diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py index b5083d34..97b01956 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py @@ -1,4 +1,4 @@ -from time_entries._domain import TimeEntryService, TimeEntry +from time_tracker.time_entries._domain import TimeEntryService, TimeEntry class GetTimeEntryUseCase: diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py index 1e801773..6c83a82e 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py @@ -1,4 +1,4 @@ -from time_entries._domain import TimeEntryService, TimeEntry +from time_tracker.time_entries._domain import TimeEntryService, TimeEntry import typing class GetTimeEntriesUseCase: diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py index dfeddb26..500006f1 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -2,7 +2,7 @@ import dataclasses import typing -from time_entries._domain import TimeEntriesDao, TimeEntry +from time_tracker.time_entries._domain import TimeEntriesDao, TimeEntry class TimeEntriesJsonDao(TimeEntriesDao): diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index c02084ba..2684c8de 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -1,2 +1,3 @@ +# flake8: noqa from ._application import create_time_entry from ._application import get_time_entries From 47edb1a6c4c314ad9e42cc28f78a058e6cca6c15 Mon Sep 17 00:00:00 2001 From: Jobzi Date: Thu, 11 Nov 2021 13:22:43 -0500 Subject: [PATCH 13/18] test: TT-404 add time entries test --- V2/tests/api/api_fixtures.py | 92 +++++++++++++++++++ .../azure/time_entry_azure_endpoints_test.py | 40 ++++++++ .../daos/time_entries_json_dao_test.py | 80 ++++++++++++++++ .../unit/services/time_entry_service_test.py | 28 ++++++ .../use_cases/time_entries_use_case_test.py | 35 +++++++ .../_data_persistence/time_entries_data.json | 14 +-- 6 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 V2/tests/api/api_fixtures.py create mode 100644 V2/tests/api/azure/time_entry_azure_endpoints_test.py create mode 100644 V2/tests/integration/daos/time_entries_json_dao_test.py create mode 100644 V2/tests/unit/services/time_entry_service_test.py create mode 100644 V2/tests/unit/use_cases/time_entries_use_case_test.py diff --git a/V2/tests/api/api_fixtures.py b/V2/tests/api/api_fixtures.py new file mode 100644 index 00000000..3f22f3ee --- /dev/null +++ b/V2/tests/api/api_fixtures.py @@ -0,0 +1,92 @@ +import json +import pytest +import shutil + + +@pytest.fixture +def create_temp_activities(tmpdir_factory): + temporary_directory = tmpdir_factory.mktemp("tmp") + json_file = temporary_directory.join("activities.json") + activities = [ + { + 'id': 'c61a4a49-3364-49a3-a7f7-0c5f2d15072b', + 'name': 'Development', + 'description': 'Development', + 'deleted': 'b4327ba6-9f96-49ee-a9ac-3c1edf525172', + 'status': 'active', + 'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05', + }, + { + 'id': '94ec92e2-a500-4700-a9f6-e41eb7b5507c', + 'name': 'Management', + 'description': 'Description of management', + 'deleted': '7cf6efe5-a221-4fe4-b94f-8945127a489a', + 'status': 'active', + 'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05', + }, + { + 'id': 'd45c770a-b1a0-4bd8-a713-22c01a23e41b', + 'name': 'Operations', + 'description': 'Operation activities performed.', + 'deleted': '7cf6efe5-a221-4fe4-b94f-8945127a489a', + 'status': 'active', + 'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05', + }, + ] + + with open(json_file, 'w') as outfile: + json.dump(activities, outfile) + + yield activities, json_file + shutil.rmtree(temporary_directory) + + +@pytest.fixture +def create_temp_time_entries(tmpdir_factory): + temporary_directory = tmpdir_factory.mktemp("tmp") + json_file = temporary_directory.join("time_entries.json") + activities = [ + { + "id": 1, + "start_date": "12/07/2021", + "owner_id": 2, + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 + }, { + "id": 2, + "start_date": "12/07/2021", + "owner_id": 2, + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 + }, { + "id": 3, + "start_date": "12/07/2021", + "owner_id": 2, + "description": "No se que poner jajaj ", + "activity_id": 2, + "uri": "http://hola.que.hace.com", + "technologies": ["git", "jira", "python"], + "end_date": "12/07/2021", + "deleted": "asdasdsaadssa", + "timezone_offset": "asdasdsa", + "project_id": 1 + } + ] + + with open(json_file, 'w') as outfile: + json.dump(activities, outfile) + + yield activities, json_file + shutil.rmtree(temporary_directory) diff --git a/V2/tests/api/azure/time_entry_azure_endpoints_test.py b/V2/tests/api/azure/time_entry_azure_endpoints_test.py new file mode 100644 index 00000000..ad5f0286 --- /dev/null +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -0,0 +1,40 @@ +from time_tracker.time_entries._application import _time_entries as time_entries + +import azure.functions as func +import json + + +TIME_ENTRY_URL = "/api/time_entries/" + + +def test__time_entry_azure_endpoint__returns_all_time_entries( + create_temp_time_entries, +): + time_entries_json, tmp_directory = create_temp_time_entries + time_entries._get_time_entries.JSON_PATH = tmp_directory + req = func.HttpRequest(method="GET", body=None, url=TIME_ENTRY_URL) + + response = time_entries.get_time_entries(req) + time_entries_json_data = response.get_body().decode("utf-8") + + assert response.status_code == 200 + assert time_entries_json_data == json.dumps(time_entries_json) + + +def test__time_entry_azure_endpoint__returns_an_time_entry__when_time_entry_matches_its_id( + create_temp_time_entries, +): + time_entries_json, tmp_directory = create_temp_time_entries + time_entries._get_time_entries.JSON_PATH = tmp_directory + req = func.HttpRequest( + method="GET", + body=None, + url=TIME_ENTRY_URL, + route_params={"id": time_entries_json[0]["id"]}, + ) + + response = time_entries.get_time_entries(req) + time_entry_json_data = response.get_body().decode("utf-8") + + assert response.status_code == 200 + assert time_entry_json_data == json.dumps(time_entries_json[0]) diff --git a/V2/tests/integration/daos/time_entries_json_dao_test.py b/V2/tests/integration/daos/time_entries_json_dao_test.py new file mode 100644 index 00000000..1920aece --- /dev/null +++ b/V2/tests/integration/daos/time_entries_json_dao_test.py @@ -0,0 +1,80 @@ +from time_tracker.time_entries._infrastructure import TimeEntriesJsonDao +from time_tracker.time_entries._domain import TimeEntry +from faker import Faker +import json +import pytest +import typing +import random + +fake_time_entries = [ + { + "id": Faker().uuid4(), + "start_date": str(Faker().date_time()), + "owner_id": Faker().uuid4(), + "description": Faker().sentence(), + "activity_id": Faker().uuid4(), + "uri": Faker().uri(), + "technologies": [Faker().name() for x in range(random.randrange(5))], + "end_date": str(Faker().date_time()), + "deleted": Faker().uuid4(), + "timezone_offset": str(Faker().date_time()), + "project_id": Faker().uuid4(), + } +] + + +@pytest.fixture(name="create_fake_time_entries") +def _create_fake_time_entries(mocker) -> typing.List[TimeEntry]: + def _creator(time_entries): + read_data = json.dumps(time_entries) + mocker.patch("builtins.open", mocker.mock_open(read_data=read_data)) + return [TimeEntry(**time_entry) for time_entry in time_entries] + + return _creator + + +def test_get_by_id__returns_an_time_entry_dto__when_found_one_time_entry_that_matches_its_id( + create_fake_time_entries, +): + print(fake_time_entries) + time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) + time_entries = create_fake_time_entries(fake_time_entries) + time_entries_dto = time_entries.pop() + + result = time_entries_json_dao.get_by_id(time_entries_dto.id) + + assert result == time_entries_dto + + +def test__get_by_id__returns_none__when_no_time_entry_matches_its_id( + create_fake_time_entries, +): + time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) + create_fake_time_entries([]) + + result = time_entries_json_dao.get_by_id(Faker().uuid4()) + + assert result is None + + +def test__get_all__returns_a_list_of_time_entry_dto_objects__when_one_or_more_time_entries_are_found( + create_fake_time_entries, +): + time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) + number_of_time_entries = 3 + time_entries = create_fake_time_entries(fake_time_entries * number_of_time_entries) + + result = time_entries_json_dao.get_all() + + assert result == time_entries + + +def test_get_all__returns_an_empty_list__when_doesnt_found_any_time_entries( + create_fake_time_entries, +): + time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) + time_entries = create_fake_time_entries([]) + + result = time_entries_json_dao.get_all() + + assert result == time_entries diff --git a/V2/tests/unit/services/time_entry_service_test.py b/V2/tests/unit/services/time_entry_service_test.py new file mode 100644 index 00000000..05f955e3 --- /dev/null +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -0,0 +1,28 @@ +from time_tracker.time_entries._domain import TimeEntryService +from faker import Faker + + +def test__get_all__uses_the_time_entry_dao__to_retrieve_time_entries(mocker): + expected_time_entries = mocker.Mock() + time_entry_dao = mocker.Mock( + get_all=mocker.Mock(return_value=expected_time_entries) + ) + time_activity_service = TimeEntryService(time_entry_dao) + + actual_activities = time_activity_service.get_all() + + assert time_entry_dao.get_all.called + assert expected_time_entries == actual_activities + + +def test__get_by_id__uses_the_time_entry_dao__to_retrieve_one_time_entry(mocker): + expected_time_entry = mocker.Mock() + time_entry_dao = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_time_entry) + ) + time_entry_service = TimeEntryService(time_entry_dao) + + actual_time_entry = time_entry_service.get_by_id(Faker().uuid4()) + + assert time_entry_dao.get_by_id.called + assert expected_time_entry == actual_time_entry diff --git a/V2/tests/unit/use_cases/time_entries_use_case_test.py b/V2/tests/unit/use_cases/time_entries_use_case_test.py new file mode 100644 index 00000000..1c38f324 --- /dev/null +++ b/V2/tests/unit/use_cases/time_entries_use_case_test.py @@ -0,0 +1,35 @@ +from time_tracker.time_entries._domain import _use_cases +from pytest_mock import MockFixture +from faker import Faker + +fake = Faker() + + +def test__get_list_time_entries_function__uses_the_time_entry_service__to_retrieve_time_entries( + mocker: MockFixture, +): + expected_time_entries = mocker.Mock() + time_entry_service = mocker.Mock( + get_all=mocker.Mock(return_value=expected_time_entries) + ) + + time_entries_use_case = _use_cases.GetTimeEntriesUseCase(time_entry_service) + actual_time_entries = time_entries_use_case.get_time_entries() + + assert time_entry_service.get_all.called + assert expected_time_entries == actual_time_entries + + +def test__get_time_entry_by_id_function__uses_the_time_entry_service__to_retrieve_time_entry( + mocker: MockFixture, +): + expected_time_entries = mocker.Mock() + time_entry_service = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_time_entries) + ) + + time_entry_use_case = _use_cases.GetTimeEntryUseCase(time_entry_service) + actual_time_entry = time_entry_use_case.get_time_entry_by_id(fake.uuid4()) + + assert time_entry_service.get_by_id.called + assert expected_time_entries == actual_time_entry diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json index 2bc98c92..c1db34aa 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json @@ -1,5 +1,5 @@ [{ - "id": null, + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072b", "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -12,7 +12,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": null, + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072c", "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -25,7 +25,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": null, + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072e", "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -38,7 +38,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": null, + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072f", "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -51,7 +51,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": null, + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072g", "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -64,7 +64,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": null, + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072h", "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -77,7 +77,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": null, + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072i", "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", From fa38bbcb9fcff4b30fa469690d27df77df44425d Mon Sep 17 00:00:00 2001 From: Jobzi Date: Thu, 11 Nov 2021 13:24:14 -0500 Subject: [PATCH 14/18] refactor: TT-404 apply flake8 --- V2/tests/conftest.py | 4 +- .../time_entries/_application/__init__.py | 3 +- .../_application/_time_entries/__init__.py | 1 + .../_time_entries/_create_time_entry.py | 67 +++++++++---------- .../_time_entries/_get_time_entries.py | 1 + .../time_entries/_domain/__init__.py | 1 + .../_domain/_entities/__init__.py | 3 +- .../_domain/_entities/_time_entry.py | 3 +- .../_persistence_contracts/__init__.py | 3 +- .../_time_entries_dao.py | 5 +- .../_domain/_services/__init__.py | 3 +- .../_domain/_services/_time_entry.py | 20 +++--- .../_domain/_use_cases/__init__.py | 3 +- .../_use_cases/_create_time_entry_use_case.py | 10 +-- .../_use_cases/_get_time_entry_use_case.py | 1 + .../time_entries/_infrastructure/__init__.py | 3 +- .../_data_persistence/__init__.py | 3 +- .../_data_persistence/_time_entries_dao.py | 5 +- 18 files changed, 77 insertions(+), 62 deletions(-) diff --git a/V2/tests/conftest.py b/V2/tests/conftest.py index d1c4928f..b455b018 100644 --- a/V2/tests/conftest.py +++ b/V2/tests/conftest.py @@ -1,2 +1,4 @@ # flake8: noqa -from fixtures import _activity_factory, _create_fake_dao, _create_fake_database \ No newline at end of file +from fixtures import _activity_factory, _create_fake_dao, _create_fake_database +from tests.api.api_fixtures import create_temp_activities +from tests.api.api_fixtures import create_temp_time_entries diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index 8516ef71..a1812c0b 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -1,2 +1,3 @@ +# flake8: noqa from ._time_entries import create_time_entry -from ._time_entries import get_time_entries \ No newline at end of file +from ._time_entries import get_time_entries diff --git a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py index 3d9e7cf8..cf2cb2cf 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -1,2 +1,3 @@ +# flake8: noqa from ._create_time_entry import create_time_entry from ._get_time_entries import get_time_entries \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py index e203f16f..b6906f3a 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py @@ -1,6 +1,4 @@ -import dataclasses import json -import typing import azure.functions as func @@ -11,39 +9,40 @@ 'time_entries/_infrastructure/_data_persistence/time_entries_data.json' ) + def create_time_entry(req: func.HttpRequest) -> func.HttpResponse: - time_entry_dao = _infrastructure.TimeEntriesJsonDao(_JSON_PATH) - time_entry_service = _domain.TimeEntryService(time_entry_dao) - use_case = _domain._use_cases.CreateTimeEntryUseCase(time_entry_service) - - time_entry_data = req.get_json() - - time_entry_to_create = _domain.TimeEntry( - id=None, - start_date=time_entry_data["start_date"], - owner_id=time_entry_data["owner_id"], - description=time_entry_data["description"], - activity_id=time_entry_data["activity_id"], - uri=time_entry_data["uri"], - technologies=time_entry_data["technologies"], - end_date=time_entry_data["end_date"], - deleted=time_entry_data["deleted"], - timezone_offset=time_entry_data["timezone_offset"], - project_id=time_entry_data["project_id"] - ) - - created_time_entry = use_case.create_time_entry(time_entry_to_create.__dict__) - - if not created_time_entry: - return func.HttpResponse( - body=json.dumps({'error': 'time_entry could not be created'}), - status_code=500, - mimetype="application/json" + time_entry_dao = _infrastructure.TimeEntriesJsonDao(_JSON_PATH) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.CreateTimeEntryUseCase(time_entry_service) + + time_entry_data = req.get_json() + + time_entry_to_create = _domain.TimeEntry( + id=None, + start_date=time_entry_data["start_date"], + owner_id=time_entry_data["owner_id"], + description=time_entry_data["description"], + activity_id=time_entry_data["activity_id"], + uri=time_entry_data["uri"], + technologies=time_entry_data["technologies"], + end_date=time_entry_data["end_date"], + deleted=time_entry_data["deleted"], + timezone_offset=time_entry_data["timezone_offset"], + project_id=time_entry_data["project_id"] ) - return func.HttpResponse( - body=json.dumps(created_time_entry.__dict__), - status_code=201, - mimetype="application/json" - ) \ No newline at end of file + created_time_entry = use_case.create_time_entry(time_entry_to_create.__dict__) + + if not created_time_entry: + return func.HttpResponse( + body=json.dumps({'error': 'time_entry could not be created'}), + status_code=500, + mimetype="application/json" + ) + + return func.HttpResponse( + body=json.dumps(created_time_entry.__dict__), + status_code=201, + mimetype="application/json" + ) diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py index 1ee18581..6c60e95f 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -8,6 +8,7 @@ 'time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json' ) + def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: time_entry_id = req.route_params.get('id') diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index 78c5ad3b..cc9ada37 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa from ._entities import TimeEntry from ._persistence_contracts import TimeEntriesDao from ._services import TimeEntryService diff --git a/V2/time_tracker/time_entries/_domain/_entities/__init__.py b/V2/time_tracker/time_entries/_domain/_entities/__init__.py index da523368..3245a461 100644 --- a/V2/time_tracker/time_entries/_domain/_entities/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_entities/__init__.py @@ -1 +1,2 @@ -from ._time_entry import TimeEntry \ No newline at end of file +# flake8: noqa +from ._time_entry import TimeEntry diff --git a/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py index f5c3c573..bba2dba0 100644 --- a/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import List + @dataclass(frozen=True) class TimeEntry: id: int @@ -13,4 +14,4 @@ class TimeEntry: end_date: str deleted: str timezone_offset: str - project_id: int \ No newline at end of file + project_id: int diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py index ccf8230e..3f17d5ee 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py @@ -1 +1,2 @@ -from ._time_entries_dao import TimeEntriesDao \ No newline at end of file +# flake8: noqa +from ._time_entries_dao import TimeEntriesDao diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py index a4688f1a..46acc038 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -3,10 +3,11 @@ from time_tracker.time_entries._domain import TimeEntry + class TimeEntriesDao(abc.ABC): @abc.abstractmethod def get_by_id(self, id: str) -> TimeEntry: - pass + pass @abc.abstractmethod def get_all(self) -> typing.List[TimeEntry]: @@ -14,4 +15,4 @@ def get_all(self) -> typing.List[TimeEntry]: @abc.abstractmethod def create(self, time_entry_data: dict) -> TimeEntry: - pass \ No newline at end of file + pass diff --git a/V2/time_tracker/time_entries/_domain/_services/__init__.py b/V2/time_tracker/time_entries/_domain/_services/__init__.py index 5eba7f77..1a06f65b 100644 --- a/V2/time_tracker/time_entries/_domain/_services/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_services/__init__.py @@ -1 +1,2 @@ -from ._time_entry import TimeEntryService \ No newline at end of file +# flake8: noqa +from ._time_entry import TimeEntryService diff --git a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py index 5eb69d88..3fc305a6 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -1,16 +1,16 @@ from time_tracker.time_entries._domain import TimeEntry, TimeEntriesDao import typing -class TimeEntryService: - - def __init__(self, time_entry_dao: TimeEntriesDao): - self.time_entry_dao = time_entry_dao - def get_by_id(self, id: str) -> TimeEntry: - return self.time_entry_dao.get_by_id(id) +class TimeEntryService: + def __init__(self, time_entry_dao: TimeEntriesDao): + self.time_entry_dao = time_entry_dao + + def get_by_id(self, id: str) -> TimeEntry: + return self.time_entry_dao.get_by_id(id) - def get_all(self) -> typing.List[TimeEntry]: - return self.time_entry_dao.get_all() + def get_all(self) -> typing.List[TimeEntry]: + return self.time_entry_dao.get_all() - def create(self, time_entry_data: dict) -> TimeEntry: - return self.time_entry_dao.create(time_entry_data) \ No newline at end of file + def create(self, time_entry_data: dict) -> TimeEntry: + return self.time_entry_dao.create(time_entry_data) diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py index e2d51f8e..a056ba52 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa from ._create_time_entry_use_case import CreateTimeEntryUseCase from ._get_time_entry_use_case import GetTimeEntriesUseCase -from ._get_time_entry_by_id_use_case import GetTimeEntryUseCase \ No newline at end of file +from ._get_time_entry_by_id_use_case import GetTimeEntryUseCase diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py index 1df7be91..f57e7ce5 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py @@ -1,9 +1,9 @@ from time_tracker.time_entries._domain import TimeEntry, TimeEntryService + class CreateTimeEntryUseCase: + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service - def __init__(self, time_entry_service: TimeEntryService): - self.time_entry_service = time_entry_service - - def create_time_entry(self, time_entry_data: dict) -> TimeEntry: - return self.time_entry_service.create(time_entry_data) \ No newline at end of file + def create_time_entry(self, time_entry_data: dict) -> TimeEntry: + return self.time_entry_service.create(time_entry_data) diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py index 6c83a82e..9b7002f0 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py @@ -1,6 +1,7 @@ from time_tracker.time_entries._domain import TimeEntryService, TimeEntry import typing + class GetTimeEntriesUseCase: def __init__(self, time_entry_service: TimeEntryService): self.time_entry_service = time_entry_service diff --git a/V2/time_tracker/time_entries/_infrastructure/__init__.py b/V2/time_tracker/time_entries/_infrastructure/__init__.py index 89ae5c03..0e90c78a 100644 --- a/V2/time_tracker/time_entries/_infrastructure/__init__.py +++ b/V2/time_tracker/time_entries/_infrastructure/__init__.py @@ -1 +1,2 @@ -from ._data_persistence import TimeEntriesJsonDao \ No newline at end of file +# flake8: noqa +from ._data_persistence import TimeEntriesJsonDao diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py index d0db4d15..91ebd7cf 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py @@ -1 +1,2 @@ -from ._time_entries_dao import TimeEntriesJsonDao \ No newline at end of file +# flake8: noqa +from ._time_entries_dao import TimeEntriesJsonDao diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py index 500006f1..ecd1936a 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -4,8 +4,9 @@ from time_tracker.time_entries._domain import TimeEntriesDao, TimeEntry + class TimeEntriesJsonDao(TimeEntriesDao): - + def __init__(self, json_data_file_path: str): self.json_data_file_path = json_data_file_path self.time_entry_key = [field.name for field in dataclasses.fields(TimeEntry)] @@ -49,4 +50,4 @@ def __get_time_entries_from_file(self) -> typing.List[dict]: def __create_time_entry_dto(self, time_entry: dict) -> TimeEntry: time_entry = {key: time_entry.get(key) for key in self.time_entry_key} - return TimeEntry(**time_entry) \ No newline at end of file + return TimeEntry(**time_entry) From f2e143582acf3b772c86a6018198eb6c6ed2078b Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Thu, 11 Nov 2021 14:26:35 -0500 Subject: [PATCH 15/18] fix: TT-404 resolve sonar cloud Using http protocol is insecure. Use https instead --- V2/tests/api/api_fixtures.py | 6 +++--- .../_data_persistence/time_entries_data.json | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/V2/tests/api/api_fixtures.py b/V2/tests/api/api_fixtures.py index 3f22f3ee..4ce51d6b 100644 --- a/V2/tests/api/api_fixtures.py +++ b/V2/tests/api/api_fixtures.py @@ -52,7 +52,7 @@ def create_temp_time_entries(tmpdir_factory): "owner_id": 2, "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -64,7 +64,7 @@ def create_temp_time_entries(tmpdir_factory): "owner_id": 2, "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -76,7 +76,7 @@ def create_temp_time_entries(tmpdir_factory): "owner_id": 2, "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json index c1db34aa..28a077cf 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json @@ -5,7 +5,7 @@ "tenant_id": "asdasdsa", "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -18,7 +18,7 @@ "tenant_id": "asdasdsa", "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -31,7 +31,7 @@ "tenant_id": "asdasdsa", "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -44,7 +44,7 @@ "tenant_id": "asdasdsa", "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -57,7 +57,7 @@ "tenant_id": "asdasdsa", "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -70,7 +70,7 @@ "tenant_id": "asdasdsa", "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", @@ -83,7 +83,7 @@ "tenant_id": "asdasdsa", "description": "No se que poner jajaj ", "activity_id": 2, - "uri": "http://hola.que.hace.com", + "uri": "https://hola.que.hace.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": "asdasdsaadssa", From 81fda9cffa098537b14c140f47f6e423326a6d0b Mon Sep 17 00:00:00 2001 From: Jobzi Date: Thu, 11 Nov 2021 17:12:08 -0500 Subject: [PATCH 16/18] fix: TT-404 search by int in get_by_id --- .../azure/time_entry_azure_endpoints_test.py | 18 ++++++++++++++++++ .../daos/time_entries_json_dao_test.py | 2 +- .../_time_entries/_get_time_entries.py | 12 ++++++++---- .../_time_entries_dao.py | 2 +- .../_domain/_services/_time_entry.py | 2 +- .../_get_time_entry_by_id_use_case.py | 2 +- .../_data_persistence/_time_entries_dao.py | 2 +- .../_data_persistence/time_entries_data.json | 14 +++++++------- 8 files changed, 38 insertions(+), 16 deletions(-) diff --git a/V2/tests/api/azure/time_entry_azure_endpoints_test.py b/V2/tests/api/azure/time_entry_azure_endpoints_test.py index ad5f0286..2ee1ebf9 100644 --- a/V2/tests/api/azure/time_entry_azure_endpoints_test.py +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -38,3 +38,21 @@ def test__time_entry_azure_endpoint__returns_an_time_entry__when_time_entry_matc assert response.status_code == 200 assert time_entry_json_data == json.dumps(time_entries_json[0]) + + +def test__get_time_entries_azure_endpoint__returns_a_status_code_400__when_time_entry_recive_invalid_id( + create_temp_time_entries, +): + tmp_directory = create_temp_time_entries + time_entries._get_time_entries.JSON_PATH = tmp_directory + req = func.HttpRequest( + method="GET", + body=None, + url=TIME_ENTRY_URL, + route_params={"id": "invalid id"}, + ) + + response = time_entries.get_time_entries(req) + + assert response.status_code == 400 + assert response.get_body() == b'Invalid Format ID' diff --git a/V2/tests/integration/daos/time_entries_json_dao_test.py b/V2/tests/integration/daos/time_entries_json_dao_test.py index 1920aece..3e52e761 100644 --- a/V2/tests/integration/daos/time_entries_json_dao_test.py +++ b/V2/tests/integration/daos/time_entries_json_dao_test.py @@ -8,7 +8,7 @@ fake_time_entries = [ { - "id": Faker().uuid4(), + "id": Faker().pyint(), "start_date": str(Faker().date_time()), "owner_id": Faker().uuid4(), "description": Faker().sentence(), diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py index 6c60e95f..0d477417 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -15,9 +15,13 @@ def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: status_code = 200 if time_entry_id: - response = _get_by_id(time_entry_id) - if response == b'Not Found': - status_code = 404 + try: + response = _get_by_id(int(time_entry_id)) + if response == b'Not Found': + status_code = 404 + except ValueError: + response = b'Invalid Format ID' + status_code = 400 else: response = _get_all() @@ -26,7 +30,7 @@ def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: ) -def _get_by_id(id: str) -> str: +def _get_by_id(id: int) -> str: time_entry_use_case = _use_cases.GetTimeEntryUseCase( _create_time_entry_service(JSON_PATH) ) diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py index 46acc038..d9a704e7 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -6,7 +6,7 @@ class TimeEntriesDao(abc.ABC): @abc.abstractmethod - def get_by_id(self, id: str) -> TimeEntry: + def get_by_id(self, id: int) -> TimeEntry: pass @abc.abstractmethod diff --git a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py index 3fc305a6..5b5adbcc 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -6,7 +6,7 @@ class TimeEntryService: def __init__(self, time_entry_dao: TimeEntriesDao): self.time_entry_dao = time_entry_dao - def get_by_id(self, id: str) -> TimeEntry: + def get_by_id(self, id: int) -> TimeEntry: return self.time_entry_dao.get_by_id(id) def get_all(self) -> typing.List[TimeEntry]: diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py index 97b01956..410233e1 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py @@ -5,5 +5,5 @@ class GetTimeEntryUseCase: def __init__(self, time_entry_service: TimeEntryService): self.time_entry_service = time_entry_service - def get_time_entry_by_id(self, id: str) -> TimeEntry: + def get_time_entry_by_id(self, id: int) -> TimeEntry: return self.time_entry_service.get_by_id(id) diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py index ecd1936a..461ee57f 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -11,7 +11,7 @@ def __init__(self, json_data_file_path: str): self.json_data_file_path = json_data_file_path self.time_entry_key = [field.name for field in dataclasses.fields(TimeEntry)] - def get_by_id(self, id: str) -> TimeEntry: + def get_by_id(self, id: int) -> TimeEntry: time_entry = { time_entry.get('id'): time_entry for time_entry in self.__get_time_entries_from_file() diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json index 28a077cf..8b9da339 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json @@ -1,5 +1,5 @@ [{ - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072b", + "id": 1, "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -12,7 +12,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072c", + "id": 2, "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -25,7 +25,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072e", + "id": 3, "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -38,7 +38,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072f", + "id": 4, "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -51,7 +51,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072g", + "id": 5, "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -64,7 +64,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072h", + "id": 6, "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", @@ -77,7 +77,7 @@ "timezone_offset": "asdasdsa", "project_id": 1 }, { - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072i", + "id": 7, "start_date": "12/07/2021", "owner_id": 2, "tenant_id": "asdasdsa", From 6346bec4b5438e21a3e4415f8c014b7ca9d01fc9 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Fri, 12 Nov 2021 16:01:08 -0500 Subject: [PATCH 17/18] fix: TT-404 remove method POST --- .../time_entries/_application/__init__.py | 1 - .../_application/_time_entries/__init__.py | 1 - .../_time_entries/_create_time_entry.py | 48 ------------------- .../time_entries/_domain/__init__.py | 1 - .../_time_entries_dao.py | 4 -- .../_domain/_services/_time_entry.py | 3 -- .../_domain/_use_cases/__init__.py | 1 - .../_use_cases/_create_time_entry_use_case.py | 9 ---- .../_data_persistence/_time_entries_dao.py | 12 ----- V2/time_tracker/time_entries/interface.py | 1 - 10 files changed, 81 deletions(-) delete mode 100644 V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py delete mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index a1812c0b..20bdbaf6 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -1,3 +1,2 @@ # flake8: noqa -from ._time_entries import create_time_entry from ._time_entries import get_time_entries diff --git a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py index cf2cb2cf..f7077b35 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -1,3 +1,2 @@ # flake8: noqa -from ._create_time_entry import create_time_entry from ._get_time_entries import get_time_entries \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py deleted file mode 100644 index b6906f3a..00000000 --- a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py +++ /dev/null @@ -1,48 +0,0 @@ -import json - -import azure.functions as func - -from ... import _domain -from ... import _infrastructure - -_JSON_PATH = ( - 'time_entries/_infrastructure/_data_persistence/time_entries_data.json' -) - - -def create_time_entry(req: func.HttpRequest) -> func.HttpResponse: - - time_entry_dao = _infrastructure.TimeEntriesJsonDao(_JSON_PATH) - time_entry_service = _domain.TimeEntryService(time_entry_dao) - use_case = _domain._use_cases.CreateTimeEntryUseCase(time_entry_service) - - time_entry_data = req.get_json() - - time_entry_to_create = _domain.TimeEntry( - id=None, - start_date=time_entry_data["start_date"], - owner_id=time_entry_data["owner_id"], - description=time_entry_data["description"], - activity_id=time_entry_data["activity_id"], - uri=time_entry_data["uri"], - technologies=time_entry_data["technologies"], - end_date=time_entry_data["end_date"], - deleted=time_entry_data["deleted"], - timezone_offset=time_entry_data["timezone_offset"], - project_id=time_entry_data["project_id"] - ) - - created_time_entry = use_case.create_time_entry(time_entry_to_create.__dict__) - - if not created_time_entry: - return func.HttpResponse( - body=json.dumps({'error': 'time_entry could not be created'}), - status_code=500, - mimetype="application/json" - ) - - return func.HttpResponse( - body=json.dumps(created_time_entry.__dict__), - status_code=201, - mimetype="application/json" - ) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index cc9ada37..f8de11c6 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -3,7 +3,6 @@ from ._persistence_contracts import TimeEntriesDao from ._services import TimeEntryService from ._use_cases import ( - CreateTimeEntryUseCase, GetTimeEntriesUseCase, GetTimeEntryUseCase ) \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py index d9a704e7..205f7926 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -12,7 +12,3 @@ def get_by_id(self, id: int) -> TimeEntry: @abc.abstractmethod def get_all(self) -> typing.List[TimeEntry]: pass - - @abc.abstractmethod - def create(self, time_entry_data: dict) -> TimeEntry: - pass diff --git a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py index 5b5adbcc..29df1aac 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -11,6 +11,3 @@ def get_by_id(self, id: int) -> TimeEntry: def get_all(self) -> typing.List[TimeEntry]: return self.time_entry_dao.get_all() - - def create(self, time_entry_data: dict) -> TimeEntry: - return self.time_entry_dao.create(time_entry_data) diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py index a056ba52..c4c7ee3b 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -1,4 +1,3 @@ # flake8: noqa -from ._create_time_entry_use_case import CreateTimeEntryUseCase from ._get_time_entry_use_case import GetTimeEntriesUseCase from ._get_time_entry_by_id_use_case import GetTimeEntryUseCase diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py deleted file mode 100644 index f57e7ce5..00000000 --- a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py +++ /dev/null @@ -1,9 +0,0 @@ -from time_tracker.time_entries._domain import TimeEntry, TimeEntryService - - -class CreateTimeEntryUseCase: - def __init__(self, time_entry_service: TimeEntryService): - self.time_entry_service = time_entry_service - - def create_time_entry(self, time_entry_data: dict) -> TimeEntry: - return self.time_entry_service.create(time_entry_data) diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py index 461ee57f..8488c6ea 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -25,18 +25,6 @@ def get_all(self) -> typing.List[TimeEntry]: for activity in self.__get_time_entries_from_file() ] - def create(self, time_entry_data: dict) -> TimeEntry: - time_entries = self.__get_time_entries_from_file() - time_entries.append(time_entry_data) - - try: - with open(self.json_data_file_path, 'w') as outfile: - json.dump(time_entries, outfile) - - return self.__create_time_entry_dto(time_entry_data) - except FileNotFoundError: - print("Can not create activity") - def __get_time_entries_from_file(self) -> typing.List[dict]: try: file = open(self.json_data_file_path) diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index 2684c8de..b0bb0396 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -1,3 +1,2 @@ # flake8: noqa -from ._application import create_time_entry from ._application import get_time_entries From c729e0367b72c52bc32617db2ca054c93def7a2e Mon Sep 17 00:00:00 2001 From: Jobzi Date: Tue, 16 Nov 2021 13:00:30 -0500 Subject: [PATCH 18/18] test: TT-404 get time entries --- V2/create_activity/function.json | 22 --- V2/delete_activity/function.json | 22 --- V2/get_activities/function.json | 22 --- V2/tests/conftest.py | 4 +- V2/tests/fixtures.py | 60 +++++-- .../daos/time_entries_json_dao_test.py | 168 +++++++++++++----- .../_data_persistence/_activities_sql_dao.py | 2 +- .../_time_entries/_get_time_entries.py | 18 +- .../_domain/_entities/_time_entry.py | 2 +- .../time_entries/_infrastructure/__init__.py | 1 + .../_data_persistence/__init__.py | 1 + .../_time_entries_sql_dao.py | 65 +++++++ V2/update_activity/function.json | 22 --- 13 files changed, 254 insertions(+), 155 deletions(-) delete mode 100644 V2/create_activity/function.json delete mode 100644 V2/delete_activity/function.json delete mode 100644 V2/get_activities/function.json create mode 100644 V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py delete mode 100644 V2/update_activity/function.json diff --git a/V2/create_activity/function.json b/V2/create_activity/function.json deleted file mode 100644 index ed3454a9..00000000 --- a/V2/create_activity/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/", - "authLevel": "anonymous", - "methods": [ - "POST" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "create_activity", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file diff --git a/V2/delete_activity/function.json b/V2/delete_activity/function.json deleted file mode 100644 index d51170fd..00000000 --- a/V2/delete_activity/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/{id}", - "authLevel": "anonymous", - "methods": [ - "DELETE" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "delete_activity", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file diff --git a/V2/get_activities/function.json b/V2/get_activities/function.json deleted file mode 100644 index ee1efe53..00000000 --- a/V2/get_activities/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/{id:?}", - "authLevel": "anonymous", - "methods": [ - "GET" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "get_activities", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file diff --git a/V2/tests/conftest.py b/V2/tests/conftest.py index b455b018..25a28e90 100644 --- a/V2/tests/conftest.py +++ b/V2/tests/conftest.py @@ -1,4 +1,4 @@ # flake8: noqa -from fixtures import _activity_factory, _create_fake_dao, _create_fake_database -from tests.api.api_fixtures import create_temp_activities +from fixtures import _activity_factory, _create_fake_database, _time_entry_factory,_insert_activity +#from tests.api.api_fixtures import create_temp_activities from tests.api.api_fixtures import create_temp_time_entries diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py index d9539035..e7c4ca51 100644 --- a/V2/tests/fixtures.py +++ b/V2/tests/fixtures.py @@ -1,17 +1,18 @@ import pytest +from faker import Faker -import time_tracker.activities._domain as domain -import time_tracker.activities._infrastructure as infrastructure +import time_tracker.activities._domain as domain_activities +import time_tracker.activities._infrastructure as infrastructure_activities +import time_tracker.time_entries._domain as domain_time_entries from time_tracker._infrastructure import DB -from faker import Faker @pytest.fixture(name='activity_factory') -def _activity_factory() -> domain.Activity: +def _activity_factory() -> domain_activities.Activity: def _make_activity( name: str = Faker().name(), description: str = Faker().sentence(), deleted: bool = False, status: int = 1 ): - activity = domain.Activity( + activity = domain_activities.Activity( id=None, name=name, description=description, @@ -22,14 +23,47 @@ def _make_activity( return _make_activity -@pytest.fixture(name='create_fake_dao') -def _create_fake_dao() -> domain.ActivitiesDao: - db_fake = DB('sqlite:///:memory:') - dao = infrastructure.ActivitiesSQLDao(db_fake) - return dao - - @pytest.fixture(name='create_fake_database') -def _create_fake_database() -> domain.ActivitiesDao: +def _create_fake_database() -> DB: db_fake = DB('sqlite:///:memory:') return db_fake + + +@pytest.fixture(name='time_entry_factory') +def _time_entry_factory() -> domain_time_entries.TimeEntry: + def _make_time_entry( + id=Faker().random_int(), + start_date=str(Faker().date_time()), + owner_id=Faker().random_int(), + description=Faker().sentence(), + activity_id=Faker().random_int(), + uri=Faker().domain_name(), + technologies=["jira", "git"], + end_date=str(Faker().date_time()), + deleted=False, + timezone_offset="300", + project_id=Faker().random_int(), + ): + time_entry = domain_time_entries.TimeEntry( + id=id, + start_date=start_date, + owner_id=owner_id, + description=description, + activity_id=activity_id, + uri=uri, + technologies=technologies, + end_date=end_date, + deleted=deleted, + timezone_offset=timezone_offset, + project_id=project_id, + ) + return time_entry + return _make_time_entry + +@pytest.fixture(name='insert_activity') +def _insert_activity() -> dict: + def _new_activity(activity: domain_activities.Activity, database: DB): + dao = infrastructure_activities.ActivitiesSQLDao(database) + new_activity = dao.create(activity) + return new_activity + return _new_activity diff --git a/V2/tests/integration/daos/time_entries_json_dao_test.py b/V2/tests/integration/daos/time_entries_json_dao_test.py index 3e52e761..361b6bd9 100644 --- a/V2/tests/integration/daos/time_entries_json_dao_test.py +++ b/V2/tests/integration/daos/time_entries_json_dao_test.py @@ -1,11 +1,17 @@ -from time_tracker.time_entries._infrastructure import TimeEntriesJsonDao -from time_tracker.time_entries._domain import TimeEntry -from faker import Faker import json import pytest import typing import random +from faker import Faker + +import time_tracker.activities._domain as domain +import time_tracker.activities._infrastructure as infrastructure +from time_tracker._infrastructure import DB + +from time_tracker.time_entries._infrastructure import TimeEntriesSQLDao +from time_tracker.time_entries._domain import TimeEntry,TimeEntriesDao + fake_time_entries = [ { "id": Faker().pyint(), @@ -21,60 +27,142 @@ "project_id": Faker().uuid4(), } ] +# @pytest.fixture(name='insert_activity') +# def _insert_activity() -> domain.Activity: +# def _new_activity(activity: domain.Activity, dao: domain.ActivitiesDao): +# new_activity = dao.create(activity) +# return new_activity +# return _new_activity -@pytest.fixture(name="create_fake_time_entries") -def _create_fake_time_entries(mocker) -> typing.List[TimeEntry]: - def _creator(time_entries): - read_data = json.dumps(time_entries) - mocker.patch("builtins.open", mocker.mock_open(read_data=read_data)) - return [TimeEntry(**time_entry) for time_entry in time_entries] - return _creator +@pytest.fixture(name='insert_time_entry') +def _insert_time_entry() -> TimeEntry: + def _new_time_entry(time_entry: TimeEntry, dao: TimeEntriesSQLDao): + _new_time_entry = dao.create(time_entry) + return _new_time_entry + return _new_time_entry +@pytest.fixture(name='create_fake_dao') +def _create_fake_dao() -> TimeEntriesSQLDao: + db_fake = DB('sqlite:///:memory:') + dao = TimeEntriesSQLDao(db_fake) + return dao -def test_get_by_id__returns_an_time_entry_dto__when_found_one_time_entry_that_matches_its_id( - create_fake_time_entries, -): - print(fake_time_entries) - time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) - time_entries = create_fake_time_entries(fake_time_entries) - time_entries_dto = time_entries.pop() - result = time_entries_json_dao.get_by_id(time_entries_dto.id) +@pytest.fixture(name='clean_database', autouse=True) +def _clean_database(): + yield + db_fake = DB('sqlite:///:memory:') + dao = infrastructure.ActivitiesSQLDao(db_fake) + query = dao.activity.delete() + dao.db.get_session().execute(query) + - assert result == time_entries_dto +# @pytest.fixture(name="create_fake_time_entries") +# def _create_fake_time_entries(mocker) -> typing.List[TimeEntry]: +# def _creator(time_entries): +# read_data = json.dumps(time_entries) +# mocker.patch("builtins.open", mocker.mock_open(read_data=read_data)) +# return [TimeEntry(**time_entry) for time_entry in time_entries] -def test__get_by_id__returns_none__when_no_time_entry_matches_its_id( - create_fake_time_entries, +# return _creator + + +def test__get_all__returns_a_list_of_time_entries_dto_objects__when_one_or_more_time_entries_are_found_in_sql_database( + create_fake_dao, time_entry_factory, insert_time_entry,insert_activity, activity_factory ): - time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) - create_fake_time_entries([]) + dao=create_fake_dao + inserted_activity = insert_activity(activity_factory(),dao.db) + print("================") + print(activity_factory()) + print(inserted_activity) + existent_time_entries = [time_entry_factory(activity_id=inserted_activity.id)] + inserted_time_entries = [insert_time_entry(existent_time_entries[0], dao)] + time_entry= dao.get_all() + + assert isinstance(time_entry, typing.List) + assert time_entry == inserted_time_entries - result = time_entries_json_dao.get_by_id(Faker().uuid4()) +# def test__get_by_id__returns_none__when_no_time_entry_matches_its_id( +# create_fake_time_entries, +# ): +# time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) +# create_fake_time_entries([]) - assert result is None +# result = time_entries_json_dao.get_by_id(Faker().uuid4()) +# assert result is None -def test__get_all__returns_a_list_of_time_entry_dto_objects__when_one_or_more_time_entries_are_found( - create_fake_time_entries, -): - time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) - number_of_time_entries = 3 - time_entries = create_fake_time_entries(fake_time_entries * number_of_time_entries) - result = time_entries_json_dao.get_all() +# def test__get_all__returns_a_list_of_time_entry_dto_objects__when_one_or_more_time_entries_are_found( +# create_fake_time_entries, +# ): +# time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) +# number_of_time_entries = 3 +# time_entries = create_fake_time_entries(fake_time_entries * number_of_time_entries) - assert result == time_entries +# result = time_entries_json_dao.get_all() +# assert result == time_entries + + +# def test_get_all__returns_an_empty_list__when_doesnt_found_any_time_entries( +# create_fake_time_entries, +# ): +# time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) +# time_entries = create_fake_time_entries([]) + +# result = time_entries_json_dao.get_all() + +# assert result == time_entries + +# def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_activities_are_found_with_sql_database( +# create_fake_dao, activity_factory, insert_activity +# ): +# dao = create_fake_dao +# existent_activities = [activity_factory(), activity_factory()] +# inserted_activities = [ +# insert_activity(existent_activities[0], dao), +# insert_activity(existent_activities[1], dao) +# ] + +# activities = dao.get_all() + +# assert isinstance(activities, typing.List) +# assert activities == inserted_activities + + +# def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matches_its_id_with_sql_database( +# create_fake_dao, activity_factory, insert_activity +# ): +# dao = create_fake_dao +# existent_activity = activity_factory() +# inserted_activity = insert_activity(existent_activity, dao) + +# activity = dao.get_by_id(inserted_activity.id) + +# assert isinstance(activity, domain.Activity) +# assert activity.id == inserted_activity.id +# assert activity == inserted_activity + + +# def test__get_by_id__returns_none__when_no_activity_matches_its_id_with_sql_database( +# create_fake_dao, activity_factory +# ): +# dao = create_fake_dao +# existent_activity = activity_factory() + +# activity = dao.get_by_id(existent_activity.id) + +# assert activity is None -def test_get_all__returns_an_empty_list__when_doesnt_found_any_time_entries( - create_fake_time_entries, -): - time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) - time_entries = create_fake_time_entries([]) - result = time_entries_json_dao.get_all() +# def test_get_all__returns_an_empty_list__when_doesnt_found_any_activities_with_sql_database( +# create_fake_dao +# ): +# activities = create_fake_dao.get_all() - assert result == time_entries +# assert isinstance(activities, typing.List) +# assert activities == [] diff --git a/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py b/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py index e69dd1a4..0d61a0bd 100644 --- a/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py +++ b/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py @@ -41,7 +41,7 @@ def get_all(self) -> typing.List[domain.Activity]: def create(self, activity_data: domain.Activity) -> domain.Activity: new_activity = activity_data.__dict__ - new_activity.pop('id', None) + #new_activity.pop('id', None) new_activity.update({"status": 1, "deleted": False}) query = self.activity.insert().values(new_activity).return_defaults() diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py index 0d477417..01c12093 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -1,13 +1,11 @@ -from time_tracker.time_entries._infrastructure import TimeEntriesJsonDao +from time_tracker.time_entries._infrastructure import TimeEntriesSQLDao from time_tracker.time_entries._domain import TimeEntryService, _use_cases +from time_tracker._infrastructure import DB import azure.functions as func import json -JSON_PATH = ( - 'time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json' -) - +DATABASE = DB() def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: @@ -32,7 +30,7 @@ def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: def _get_by_id(id: int) -> str: time_entry_use_case = _use_cases.GetTimeEntryUseCase( - _create_time_entry_service(JSON_PATH) + _create_time_entry_service(DATABASE) ) time_entry = time_entry_use_case.get_time_entry_by_id(id) @@ -41,7 +39,7 @@ def _get_by_id(id: int) -> str: def _get_all() -> str: time_entries_use_case = _use_cases.GetTimeEntriesUseCase( - _create_time_entry_service(JSON_PATH) + _create_time_entry_service(DATABASE) ) return json.dumps( [ @@ -51,6 +49,6 @@ def _get_all() -> str: ) -def _create_time_entry_service(path: str): - time_entry_json = TimeEntriesJsonDao(path) - return TimeEntryService(time_entry_json) +def _create_time_entry_service(db: DB): + time_entry_sql = TimeEntriesSQLDao(db) + return TimeEntryService(time_entry_sql) diff --git a/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py index bba2dba0..aa73a879 100644 --- a/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py @@ -12,6 +12,6 @@ class TimeEntry: uri: str technologies: List[str] end_date: str - deleted: str + deleted: bool timezone_offset: str project_id: int diff --git a/V2/time_tracker/time_entries/_infrastructure/__init__.py b/V2/time_tracker/time_entries/_infrastructure/__init__.py index 0e90c78a..c9f22c35 100644 --- a/V2/time_tracker/time_entries/_infrastructure/__init__.py +++ b/V2/time_tracker/time_entries/_infrastructure/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa +from ._data_persistence import TimeEntriesSQLDao from ._data_persistence import TimeEntriesJsonDao diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py index 91ebd7cf..a96060a6 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa +from ._time_entries_sql_dao import TimeEntriesSQLDao from ._time_entries_dao import TimeEntriesJsonDao diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py new file mode 100644 index 00000000..6e130e69 --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py @@ -0,0 +1,65 @@ +import dataclasses +import typing + +import sqlalchemy +import sqlalchemy.sql as sql + +import time_tracker.time_entries._domain as domain +from time_tracker._infrastructure import _db + + +class TimeEntriesSQLDao(domain.TimeEntriesDao): + + def __init__(self, database: _db.DB): + self.time_entries_keys = [ + field.name for field in dataclasses.fields(domain.TimeEntry) + ] + self.db = database + + self.time_entry = sqlalchemy.Table( + 'time_entry', + self.db.metadata, + sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True, autoincrement=True), + sqlalchemy.Column('start_date', sqlalchemy.DateTime().with_variant(sqlalchemy.String,"sqlite")), + sqlalchemy.Column('owner_id', sqlalchemy.Integer), + sqlalchemy.Column('description', sqlalchemy.String), + sqlalchemy.Column('activity_id', sqlalchemy.Integer, sqlalchemy.ForeignKey('activity.id')), + sqlalchemy.Column('uri',sqlalchemy.String), + sqlalchemy.Column('technologies',sqlalchemy.ARRAY(sqlalchemy.String).with_variant(sqlalchemy.String,"sqlite")), + sqlalchemy.Column('end_date',sqlalchemy.DateTime().with_variant(sqlalchemy.String,"sqlite")), + sqlalchemy.Column('deleted',sqlalchemy.Boolean), + sqlalchemy.Column('timezone_offset',sqlalchemy.String), + sqlalchemy.Column('project_id',sqlalchemy.Integer), + extend_existing=True, + ) + + def get_by_id(self, time_entry_id: int) -> domain.TimeEntry: + query = sql.select(self.time_entry).where(self.time_entry.c.id == time_entry_id) + time_entry = self.db.get_session().execute(query).one_or_none() + return self.__create_time_entry_dto(dict(time_entry)) if time_entry else None + + def get_all(self) -> typing.List[domain.TimeEntry]: + query = sql.select(self.time_entry) + result = self.db.get_session().execute(query) + return [ + self.__create_time_entry_dto(dict(time_entry)) + for time_entry in result + ] + + def create(self, time_entry_data: domain.TimeEntry) -> domain.TimeEntry: + try: + new_time_entry = time_entry_data.__dict__ + new_time_entry.pop('id', None) + + query = self.time_entry.insert().values(new_time_entry).return_defaults() + time_entry = self.db.get_session().execute(query) + new_time_entry.update({"id": time_entry.inserted_primary_key[0]}) + return self.__create_time_entry_dto(new_time_entry) + + except sqlalchemy.exc.SQLAlchemyError: + return None + + def __create_time_entry_dto(self, time_entry: dict) -> domain.TimeEntry: + time_entry.update({"start_date": str(time_entry.get("start_date")),"end_date": str(time_entry.get("end_date"))}) + time_entry = {key: time_entry.get(key) for key in self.time_entries_keys} + return domain.TimeEntry(**time_entry) diff --git a/V2/update_activity/function.json b/V2/update_activity/function.json deleted file mode 100644 index 97c9fb49..00000000 --- a/V2/update_activity/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/{id}", - "authLevel": "anonymous", - "methods": [ - "PUT" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "update_activity", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file