From 1448fc2bc8dce7d8f50c758a910182d7fe9c011a Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Wed, 17 Nov 2021 16:00:01 -0500 Subject: [PATCH 1/5] ci: TT-411 inject secrets environment and test_db_connection (#351) --- .github/workflows/python-package.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 36bff27a..2f64bc87 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -29,6 +29,14 @@ jobs: run: | pip install -r V2/requirements.txt + - name: Inject Secrets + env: + ENVIRONMENT: ${{ secrets.environment }} + TEST_DB_CONNECTION: ${{ secrets.test_db_connection }} + run: | + echo $ENVIRONMENT + echo $TEST_DB_CONNECTION + - name: Lint with flake8 run: | cd V2 From 32ee36f39e81866c2f0767cf243c61afde6841c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Cobe=C3=B1a=20Cede=C3=B1o?= Date: Thu, 18 Nov 2021 10:05:00 -0500 Subject: [PATCH 2/5] feat: TT-399 Config use makefile to executing tests (#350) * feat: TT-399 Config use makefile to executing tests * feat: TT-399 quit comment on line for pip upgrade * fix: TT-399 inject environment variable for tests Co-authored-by: Alexander --- V2/Makefile | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/V2/Makefile b/V2/Makefile index 45080238..135e96d0 100644 --- a/V2/Makefile +++ b/V2/Makefile @@ -1,8 +1,33 @@ +.PHONY: help +help: + @echo "---------------HELP-----------------" + @echo "To install the dependencies type make install" + @echo "To test the project type make test" + @echo "To run the local database type make start-local" + @echo "To run all comands type make ci" + @echo "------------------------------------" + +.PHONY: install install: - @echo "Installing Time Tracker" + @echo "=========================================Installing dependencies Time Tracker=========================================" npm install pip install --upgrade pip pip install -r requirements.txt @echo "Completed! " + +.PHONY: test +test: export ENVIRONMENT = test +test: export TEST_DB_CONNECTION = sqlite:///:memory: +test: + @echo "=========================================Lint with flake8=========================================" + flake8 . --show-source --statistics + @echo "Completed flake8!" + @echo "=========================================Test with pytest=========================================" + python -m pytest -v + @echo "Completed test!" + start-local: - docker compose up \ No newline at end of file + docker compose up + +.PHONY: ci +ci: install test \ No newline at end of file From 10cc4269e4e60c6eff77bf1cf02cdf0d31dac86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Cobe=C3=B1a=20Cede=C3=B1o?= Date: Thu, 18 Nov 2021 17:49:50 -0500 Subject: [PATCH 3/5] docs: TT-399 Readme update how to use makefile (#354) * docs: TT-399 Readme update how to use makefile * docs: TT-399 Readme update reqs to use makefile * docs: TT-399 Text correction --- V2/README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/V2/README.md b/V2/README.md index e84c0268..f414079d 100644 --- a/V2/README.md +++ b/V2/README.md @@ -1,3 +1,23 @@ -# Azure Functions +# time-tracker-api V2 Refer to [Serverless docs](https://serverless.com/framework/docs/providers/azure/guide/intro/) for more information. + +## Requirements to use makefile + +- Python version 3.6 or 3.7. + +- Use an environment to install requirements (pyenv). + +## How to use makefile + +Execute the next command to show makefile help: + +```shell +make help +``` + +- To install the dependencies type the command ```make install``` + +- To test the project type the command ```make test``` + +- To run the local database type the command ```make start-local``` From 5f107f33cb640f7fa8e498db2157efb2d11f401d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Rafael=20Arcos=20G=C3=B3mez?= <37599693+ararcos@users.noreply.github.com> Date: Fri, 19 Nov 2021 09:37:54 -0500 Subject: [PATCH 4/5] feat: TT-401-Post-v2-time-entries (#344) * feat: TT-401 Implemented service, end-point, dao, test- time entries * feat: TT-401 validated request create time entry * fix: TT-401 implemented faker url * refactor: TT-401 changed the variable name * fix: implementation of the database connection * refactor: TT-401 fixtures changed * fix: TT-401 solution of comments, change of config * ci: TT-401 fix inject secrets * refactor: TT-401 rename of functions and imports * fix: TT-401 changed test db storage --- .github/workflows/python-package.yml | 11 +--- V2/create_activity/function.json | 22 ------- V2/delete_activity/function.json | 22 ------- V2/get_activities/function.json | 22 ------- V2/serverless.yml | 10 +++ .../azure/activity_azure_endpoints_test.py | 49 +++++--------- .../azure/time_entry_azure_endpoints_test.py | 28 ++++++++ V2/tests/conftest.py | 3 +- V2/tests/fixtures.py | 64 +++++++++++++++---- .../daos/activities_sql_dao_test.py | 21 +++--- .../integration/daos/time_entries_dao_test.py | 48 ++++++++++++++ .../unit/services/time_entry_service_test.py | 14 ++++ .../use_cases/time_entries_use_case_test.py | 18 ++++++ V2/time_tracker/_infrastructure/_config.py | 15 ++--- V2/time_tracker/_infrastructure/_db.py | 2 +- .../time_entries/_application/__init__.py | 2 + .../_application/_time_entries/__init__.py | 2 + .../_time_entries/_create_time_entry.py | 63 ++++++++++++++++++ .../time_entries/_domain/__init__.py | 7 ++ .../_domain/_entities/__init__.py | 2 + .../_domain/_entities/_time_entry.py | 17 +++++ .../_persistence_contracts/__init__.py | 2 + .../_time_entries_dao.py | 9 +++ .../_domain/_services/__init__.py | 2 + .../_domain/_services/_time_entry.py | 10 +++ .../_domain/_use_cases/__init__.py | 2 + .../_use_cases/_create_time_entry_use_case.py | 10 +++ .../time_entries/_infrastructure/__init__.py | 2 + .../_data_persistence/__init__.py | 2 + .../_data_persistence/_time_entries_dao.py | 49 ++++++++++++++ V2/time_tracker/time_entries/interface.py | 2 + V2/update_activity/function.json | 22 ------- 32 files changed, 388 insertions(+), 166 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/tests/api/azure/time_entry_azure_endpoints_test.py create mode 100644 V2/tests/integration/daos/time_entries_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 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/_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/_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/interface.py delete mode 100644 V2/update_activity/function.json diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2f64bc87..1c700563 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -14,6 +14,9 @@ jobs: runs-on: ubuntu-latest strategy: max-parallel: 5 + env: + ENVIRONMENT: ${{ secrets.ENVIRONMENT }} + TEST_DB_CONNECTION: ${{ secrets.TEST_DB_CONNECTION }} steps: - uses: actions/checkout@v2 - name: Set up Python 3.10.0 @@ -29,14 +32,6 @@ jobs: run: | pip install -r V2/requirements.txt - - name: Inject Secrets - env: - ENVIRONMENT: ${{ secrets.environment }} - TEST_DB_CONNECTION: ${{ secrets.test_db_connection }} - run: | - echo $ENVIRONMENT - echo $TEST_DB_CONNECTION - - name: Lint with flake8 run: | cd V2 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/serverless.yml b/V2/serverless.yml index 0eb3f42f..c6c5e34b 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -76,3 +76,13 @@ functions: - POST route: activities/ authLevel: anonymous + + create_time_entry: + handler: time_tracker/time_entries/interface.create_time_entry + events: + - http: true + x-azure-settings: + methods: + - POST + route: time-entries/ + authLevel: anonymous diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index 9b2618a8..994c74c7 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -1,37 +1,24 @@ -import pytest import json from faker import Faker import azure.functions as func 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/' -@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_fake_database, activity_factory, insert_activity + test_db, activity_factory, insert_activity ): - 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) + insert_activity(existent_activities[0], test_db).__dict__, + insert_activity(existent_activities[1], test_db).__dict__ ] - azure_activities._get_activities.DATABASE = fake_database + azure_activities._get_activities.DATABASE = test_db + 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") @@ -41,13 +28,12 @@ def test__activity_azure_endpoint__returns_all_activities( def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_its_id( - create_fake_database, activity_factory, insert_activity + test_db, activity_factory, insert_activity ): - fake_database = create_fake_database existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, fake_database) + inserted_activity = insert_activity(existent_activity, test_db).__dict__ - azure_activities._get_activities.DATABASE = fake_database + azure_activities._get_activities.DATABASE = test_db req = func.HttpRequest( method='GET', body=None, @@ -63,13 +49,12 @@ def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_it def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__when_an_activity_matching_its_id_is_found( - create_fake_database, activity_factory, insert_activity + test_db, activity_factory, insert_activity ): - fake_database = create_fake_database existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, fake_database) + inserted_activity = insert_activity(existent_activity, test_db).__dict__ - azure_activities._delete_activity.DATABASE = fake_database + azure_activities._delete_activity.DATABASE = test_db req = func.HttpRequest( method='DELETE', body=None, @@ -86,13 +71,12 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_activity_to_update( - create_fake_database, activity_factory, insert_activity + test_db, activity_factory, insert_activity ): - fake_database = create_fake_database existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, fake_database) + inserted_activity = insert_activity(existent_activity, test_db).__dict__ - azure_activities._update_activity.DATABASE = fake_database + azure_activities._update_activity.DATABASE = test_db activity_body = {"description": Faker().sentence()} req = func.HttpRequest( method='PUT', @@ -109,10 +93,7 @@ def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_act assert activitiy_json_data == json.dumps(inserted_activity) -def test__activity_azure_endpoint__creates_an_activity__when_activity_has_all_attributes( - create_fake_database, - ): - azure_activities._create_activity.DATABASE = create_fake_database +def test__activity_azure_endpoint__creates_an_activity__when_activity_has_all_attributes(): activity_body = { 'id': None, 'name': Faker().user_name(), 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..f801dad9 --- /dev/null +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -0,0 +1,28 @@ +import json + +import azure.functions as func + +import time_tracker.time_entries._application._time_entries as azure_time_entries + +TIME_ENTRY_URL = "/api/time-entries/" + + +def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_all_attributes( + test_db, time_entry_factory, activity_factory, insert_activity +): + inserted_activity = insert_activity(activity_factory(), test_db) + time_entry_body = time_entry_factory(activity_id=inserted_activity.id, technologies="[jira,sql]").__dict__ + + body = json.dumps(time_entry_body).encode("utf-8") + req = func.HttpRequest( + method='POST', + body=body, + url=TIME_ENTRY_URL, + ) + + response = azure_time_entries._create_time_entry.create_time_entry(req) + time_entry_json_data = json.loads(response.get_body()) + time_entry_body['id'] = time_entry_json_data['id'] + + assert response.status_code == 201 + assert time_entry_json_data == time_entry_body diff --git a/V2/tests/conftest.py b/V2/tests/conftest.py index d1c4928f..cf6e362f 100644 --- a/V2/tests/conftest.py +++ b/V2/tests/conftest.py @@ -1,2 +1,3 @@ # flake8: noqa -from fixtures import _activity_factory, _create_fake_dao, _create_fake_database \ No newline at end of file +from fixtures import _activity_factory, _test_db, _insert_activity +from fixtures import _time_entry_factory diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py index d9539035..51ee5e5d 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 activities_domain +import time_tracker.activities._infrastructure as activities_infrastructure +import time_tracker.time_entries._domain as time_entries_domain from time_tracker._infrastructure import DB -from faker import Faker @pytest.fixture(name='activity_factory') -def _activity_factory() -> domain.Activity: +def _activity_factory() -> activities_domain.Activity: def _make_activity( name: str = Faker().name(), description: str = Faker().sentence(), deleted: bool = False, status: int = 1 ): - activity = domain.Activity( + activity = activities_domain.Activity( id=None, name=name, description=description, @@ -22,14 +23,49 @@ 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='test_db') +def _test_db() -> DB: + db_fake = DB() + db_fake.get_session().execute("pragma foreign_keys=ON") + return db_fake -@pytest.fixture(name='create_fake_database') -def _create_fake_database() -> domain.ActivitiesDao: - db_fake = DB('sqlite:///:memory:') - return db_fake +@pytest.fixture(name='time_entry_factory') +def _time_entry_factory() -> time_entries_domain.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 = time_entries_domain.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: activities_domain.Activity, database: DB): + dao = activities_infrastructure.ActivitiesSQLDao(database) + new_activity = dao.create(activity) + return new_activity + return _new_activity diff --git a/V2/tests/integration/daos/activities_sql_dao_test.py b/V2/tests/integration/daos/activities_sql_dao_test.py index 25f62500..0f0170af 100644 --- a/V2/tests/integration/daos/activities_sql_dao_test.py +++ b/V2/tests/integration/daos/activities_sql_dao_test.py @@ -7,12 +7,11 @@ 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='create_fake_dao') +def _create_fake_dao() -> domain.ActivitiesDao: + db_fake = DB('sqlite:///:memory:') + dao = infrastructure.ActivitiesSQLDao(db_fake) + return dao @pytest.fixture(name='clean_database', autouse=True) @@ -41,7 +40,7 @@ def test_update__returns_an_update_activity__when_an_activity_matching_its_id_is ): dao = create_fake_dao existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, dao) + inserted_activity = insert_activity(existent_activity, dao.db) expected_description = Faker().sentence() updated_activity = dao.update(inserted_activity.id, None, expected_description, None, None) @@ -68,8 +67,8 @@ def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_acti dao = create_fake_dao existent_activities = [activity_factory(), activity_factory()] inserted_activities = [ - insert_activity(existent_activities[0], dao), - insert_activity(existent_activities[1], dao) + insert_activity(existent_activities[0], dao.db), + insert_activity(existent_activities[1], dao.db) ] activities = dao.get_all() @@ -83,7 +82,7 @@ def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matche ): dao = create_fake_dao existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, dao) + inserted_activity = insert_activity(existent_activity, dao.db) activity = dao.get_by_id(inserted_activity.id) @@ -117,7 +116,7 @@ def test_delete__returns_an_activity_with_inactive_status__when_an_activity_matc ): dao = create_fake_dao existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, dao) + inserted_activity = insert_activity(existent_activity, dao.db) activity = dao.delete(inserted_activity.id) diff --git a/V2/tests/integration/daos/time_entries_dao_test.py b/V2/tests/integration/daos/time_entries_dao_test.py new file mode 100644 index 00000000..403f80c6 --- /dev/null +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -0,0 +1,48 @@ +import pytest + + +import time_tracker.time_entries._domain as domain +import time_tracker.time_entries._infrastructure as infrastructure +from time_tracker._infrastructure import DB + + +@pytest.fixture(name='create_fake_dao') +def _fake_dao() -> domain.TimeEntriesDao: + def _create_fake_dao(db_fake: DB) -> domain.TimeEntriesDao: + dao = infrastructure.TimeEntriesSQLDao(db_fake) + return dao + return _create_fake_dao + + +@pytest.fixture(name='clean_database', autouse=True) +def _clean_database(): + yield + db_fake = DB() + dao = infrastructure.TimeEntriesSQLDao(db_fake) + query = dao.time_entry.delete() + dao.db.get_session().execute(query) + + +def test__time_entry__returns_a_time_entry_dto__when_saves_correctly_with_sql_database( + test_db, time_entry_factory, create_fake_dao, insert_activity, activity_factory +): + dao = create_fake_dao(test_db) + inserted_activity = insert_activity(activity_factory(), dao.db) + + time_entry_to_insert = time_entry_factory(activity_id=inserted_activity.id, technologies="[jira,sql]") + + inserted_time_entry = dao.create(time_entry_to_insert) + + assert isinstance(inserted_time_entry, domain.TimeEntry) + assert inserted_time_entry == time_entry_to_insert + + +def test__time_entry__returns_None__when_not_saves_correctly( + time_entry_factory, create_fake_dao, test_db +): + dao = create_fake_dao(test_db) + time_entry_to_insert = time_entry_factory(activity_id=1203, technologies="[jira,sql]") + + inserted_time_entry = dao.create(time_entry_to_insert) + + assert inserted_time_entry is None 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..bd5ce085 --- /dev/null +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -0,0 +1,14 @@ +from time_tracker.time_entries._domain import TimeEntryService + + +def test__create_time_entries__uses_the_time_entry_dao__to_create_an_time_entry(mocker, time_entry_factory): + expected_time_entry = mocker.Mock() + time_entry_dao = mocker.Mock( + create=mocker.Mock(return_value=expected_time_entry) + ) + time_entry_service = TimeEntryService(time_entry_dao) + + actual_time_entry = time_entry_service.create(time_entry_factory()) + + assert time_entry_dao.create.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..d2a31eb7 --- /dev/null +++ b/V2/tests/unit/use_cases/time_entries_use_case_test.py @@ -0,0 +1,18 @@ +from pytest_mock import MockFixture + +from time_tracker.time_entries._domain import _use_cases + + +def test__create_time_entry_function__uses_the_time_entries_service__to_create_time_entry( + mocker: MockFixture, time_entry_factory +): + expected_time_entry = mocker.Mock() + time_entry_service = mocker.Mock( + create=mocker.Mock(return_value=expected_time_entry) + ) + + time_entry_use_case = _use_cases.CreateTimeEntryUseCase(time_entry_service) + actual_time_entry = time_entry_use_case.create_time_entry(time_entry_factory()) + + assert time_entry_service.create.called + assert expected_time_entry == actual_time_entry diff --git a/V2/time_tracker/_infrastructure/_config.py b/V2/time_tracker/_infrastructure/_config.py index 7f8c8fa7..cf4f19bf 100644 --- a/V2/time_tracker/_infrastructure/_config.py +++ b/V2/time_tracker/_infrastructure/_config.py @@ -1,20 +1,17 @@ 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(): + if os.environ.get("ENVIRONMENT") == "development": + connection: str = os.environ.get("DB_CONNECTION") + else: + connection: str = os.environ.get("TEST_DB_CONNECTION") + 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") + connection ) diff --git a/V2/time_tracker/_infrastructure/_db.py b/V2/time_tracker/_infrastructure/_db.py index 8fe5cef1..6f3a9f9a 100644 --- a/V2/time_tracker/_infrastructure/_db.py +++ b/V2/time_tracker/_infrastructure/_db.py @@ -14,7 +14,7 @@ def __init__(self, conn_string: str = conn_string): self.engine = sqlalchemy.create_engine(conn_string) def get_session(self): + self.metadata.create_all(self.engine) 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/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py new file mode 100644 index 00000000..6e4ba9c3 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +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..b46cddce --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +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..a06c212c --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py @@ -0,0 +1,63 @@ +import dataclasses +import json +import typing + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +def create_time_entry(req: func.HttpRequest) -> func.HttpResponse: + database = DB() + time_entry_dao = _infrastructure.TimeEntriesSQLDao(database) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.CreateTimeEntryUseCase(time_entry_service) + + time_entry_data = req.get_json() + + validation_errors = _validate_time_entry(time_entry_data) + if validation_errors: + return func.HttpResponse( + body=json.dumps(validation_errors), status_code=400, mimetype="application/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=False, + 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) + + 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" + ) + + +def _validate_time_entry(time_entry_data: dict) -> typing.List[str]: + time_entry_fields = [field.name for field in dataclasses.fields(_domain.TimeEntry)] + time_entry_fields.pop(8) + missing_keys = [field for field in time_entry_fields if field not in time_entry_data] + return [ + f'The {missing_key} key is missing in the input data' + for missing_key in missing_keys + ] 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..a8b2081c --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa +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..88b4a739 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_entities/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +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..aa73a879 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py @@ -0,0 +1,17 @@ +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: bool + timezone_offset: str + 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 new file mode 100644 index 00000000..e10700ce --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +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..5d04c861 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -0,0 +1,9 @@ +import abc + +from time_tracker.time_entries._domain import TimeEntry + + +class TimeEntriesDao(abc.ABC): + @abc.abstractmethod + def create(self, time_entry_data: TimeEntry) -> TimeEntry: + pass 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..e5e6ba1b --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_services/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +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..d7aaf3ba --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -0,0 +1,10 @@ +from time_tracker.time_entries._domain import TimeEntry, TimeEntriesDao + + +class TimeEntryService: + + def __init__(self, time_entry_dao: TimeEntriesDao): + self.time_entry_dao = time_entry_dao + + def create(self, time_entry_data: TimeEntry) -> 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 new file mode 100644 index 00000000..41aca738 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._create_time_entry_use_case import CreateTimeEntryUseCase 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..f2258468 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py @@ -0,0 +1,10 @@ +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: TimeEntry) -> TimeEntry: + return self.time_entry_service.create(time_entry_data) 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..1c7a7d6d --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._data_persistence import TimeEntriesSQLDao 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..b999febe --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._time_entries_dao import TimeEntriesSQLDao 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..d233f3e9 --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -0,0 +1,49 @@ +import dataclasses + +import sqlalchemy + +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_entry_key = [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 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 = {key: time_entry.get(key) for key in self.time_entry_key} + return domain.TimeEntry(**time_entry) diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py new file mode 100644 index 00000000..d0182780 --- /dev/null +++ b/V2/time_tracker/time_entries/interface.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._application import create_time_entry \ No newline at end of file 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 From f65527dfdb055f45d7e6e8c52807c30b2de1d23c Mon Sep 17 00:00:00 2001 From: mandres2015 Date: Fri, 19 Nov 2021 16:45:03 -0500 Subject: [PATCH 5/5] feat: TT-407 get latest entries added --- V2/serverless.yml | 10 ++++ .../azure/time_entry_azure_endpoints_test.py | 53 +++++++++++++++++++ .../integration/daos/time_entries_dao_test.py | 28 +++++++++- .../unit/services/time_entry_service_test.py | 16 ++++++ .../use_cases/time_entries_use_case_test.py | 14 +++++ .../time_entries/_application/__init__.py | 3 +- .../_application/_time_entries/__init__.py | 3 +- .../_time_entries/_get_latest_entries.py | 47 ++++++++++++++++ .../time_entries/_domain/__init__.py | 1 + .../_time_entries_dao.py | 5 ++ .../_domain/_services/_time_entry.py | 4 ++ .../_domain/_use_cases/__init__.py | 1 + .../_get_latest_entries_use_case.py | 11 ++++ .../_data_persistence/_time_entries_dao.py | 16 +++++- V2/time_tracker/time_entries/interface.py | 3 +- 15 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py create mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/_get_latest_entries_use_case.py diff --git a/V2/serverless.yml b/V2/serverless.yml index c6c5e34b..75d6ea33 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -86,3 +86,13 @@ functions: - POST route: time-entries/ authLevel: anonymous + + get_latest_time_entry: + handler: time_tracker/time_entries/interface.get_latest_entries + events: + - http: true + x-azure-settings: + methods: + - GET + route: time-entries/latest + authLevel: anonymous 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 f801dad9..f30db215 100644 --- a/V2/tests/api/azure/time_entry_azure_endpoints_test.py +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -1,12 +1,26 @@ import json +import pytest +from faker import Faker import azure.functions as func import time_tracker.time_entries._application._time_entries as azure_time_entries +from time_tracker.time_entries import _domain +from time_tracker.time_entries import _infrastructure +from time_tracker._infrastructure import DB TIME_ENTRY_URL = "/api/time-entries/" +@pytest.fixture(name='insert_time_entry') +def _insert_time_entry() -> _domain.TimeEntry: + def _new_time_entry(time_entry: _domain.TimeEntry, database: DB): + dao = _infrastructure.TimeEntriesSQLDao(database) + new_time_entry = dao.create(time_entry) + return new_time_entry.__dict__ + return _new_time_entry + + def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_all_attributes( test_db, time_entry_factory, activity_factory, insert_activity ): @@ -26,3 +40,42 @@ def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_ assert response.status_code == 201 assert time_entry_json_data == time_entry_body + + +def test__get_latest_entries_azure_endpoint__returns_a_list_of_latest_time_entries__when_an_owner_id_match( + test_db, time_entry_factory, insert_time_entry, insert_activity, activity_factory, +): + inserted_activity = insert_activity(activity_factory(), test_db).__dict__ + time_entry_body = time_entry_factory(activity_id=inserted_activity["id"], technologies="[jira,sql]") + inserted_time_entry = insert_time_entry(time_entry_body, test_db) + + req = func.HttpRequest( + method='GET', + body=None, + url=TIME_ENTRY_URL+"latest/", + params={"owner_id": inserted_time_entry["owner_id"]}, + ) + + response = azure_time_entries._get_latest_entries.get_latest_entries(req) + time_entry_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == 200 + assert time_entry_json_data == [inserted_time_entry] + + +def test__get_latest_entries_azure_endpoint__returns_No_time_entries_found__when_recieve_an_invalid_owner_id( + test_db, insert_activity, activity_factory, +): + insert_activity(activity_factory(), test_db) + + req = func.HttpRequest( + method='GET', + body=None, + url=TIME_ENTRY_URL+"latest/", + params={"owner_id": Faker().pyint()}, + ) + + response = azure_time_entries._get_latest_entries.get_latest_entries(req) + + assert response.status_code == 404 + assert response.get_body() == b'No time entries found' diff --git a/V2/tests/integration/daos/time_entries_dao_test.py b/V2/tests/integration/daos/time_entries_dao_test.py index 403f80c6..756359d2 100644 --- a/V2/tests/integration/daos/time_entries_dao_test.py +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -1,5 +1,5 @@ import pytest - +from faker import Faker import time_tracker.time_entries._domain as domain import time_tracker.time_entries._infrastructure as infrastructure @@ -46,3 +46,29 @@ def test__time_entry__returns_None__when_not_saves_correctly( inserted_time_entry = dao.create(time_entry_to_insert) assert inserted_time_entry is None + + +def test_get_latest_entries__returns_a_list_of_latest_time_entries__when_an_owner_id_match( + create_fake_dao, time_entry_factory, insert_activity, activity_factory, test_db +): + dao = create_fake_dao(test_db) + inserted_activity = insert_activity(activity_factory(), dao.db) + time_entry_to_insert = time_entry_factory( + activity_id=inserted_activity.id, + technologies="[jira,sql]") + inserted_time_entry = dao.create(time_entry_to_insert) + + result = dao.get_latest_entries(int(inserted_time_entry.owner_id)) + + assert result == [inserted_time_entry.__dict__] + + +def test_get_latest_entries__returns_None__when_an_owner_id_is_not_found( + create_fake_dao, test_db, insert_activity, activity_factory +): + dao = create_fake_dao(test_db) + insert_activity(activity_factory(), dao.db) + + result = dao.get_latest_entries(Faker().pyint()) + + assert result is None diff --git a/V2/tests/unit/services/time_entry_service_test.py b/V2/tests/unit/services/time_entry_service_test.py index bd5ce085..57505225 100644 --- a/V2/tests/unit/services/time_entry_service_test.py +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -1,4 +1,5 @@ from time_tracker.time_entries._domain import TimeEntryService +from faker import Faker def test__create_time_entries__uses_the_time_entry_dao__to_create_an_time_entry(mocker, time_entry_factory): @@ -12,3 +13,18 @@ def test__create_time_entries__uses_the_time_entry_dao__to_create_an_time_entry( assert time_entry_dao.create.called assert expected_time_entry == actual_time_entry + + +def test__get_latest_entries__uses_the_time_entry_dao__to_get_last_entries( + mocker, +): + expected_latest_time_entries = mocker.Mock() + time_entry_dao = mocker.Mock( + get_latest_entries=mocker.Mock(return_value=expected_latest_time_entries) + ) + + time_entry_service = TimeEntryService(time_entry_dao) + latest_time_entries = time_entry_service.get_latest_entries(Faker().pyint(), Faker().pyint()) + + assert expected_latest_time_entries == latest_time_entries + assert time_entry_dao.get_latest_entries.called 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 index d2a31eb7..8b342206 100644 --- a/V2/tests/unit/use_cases/time_entries_use_case_test.py +++ b/V2/tests/unit/use_cases/time_entries_use_case_test.py @@ -1,4 +1,5 @@ from pytest_mock import MockFixture +from faker import Faker from time_tracker.time_entries._domain import _use_cases @@ -16,3 +17,16 @@ def test__create_time_entry_function__uses_the_time_entries_service__to_create_t assert time_entry_service.create.called assert expected_time_entry == actual_time_entry + + +def test__get_latest_entries_function__uses_the_time_entry_service__to_get_last_entries( + mocker: MockFixture, +): + expected_latest_time_entries = mocker.Mock() + time_entry_service = mocker.Mock(get_latest_entries=mocker.Mock(return_value=expected_latest_time_entries)) + + time_entry_use_case = _use_cases.GetLastestTimeEntryUseCase(time_entry_service) + latest_time_entries = time_entry_use_case.get_latest_entries(Faker().pyint(), Faker().pyint()) + + assert time_entry_service.get_latest_entries.called + assert expected_latest_time_entries == latest_time_entries diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index 6e4ba9c3..dd07d9d9 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 \ No newline at end of file +from ._time_entries import create_time_entry +from ._time_entries import get_latest_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 b46cddce..e6583892 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 \ No newline at end of file +from ._create_time_entry import create_time_entry +from ._get_latest_entries import get_latest_entries \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py new file mode 100644 index 00000000..8c7e8a10 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py @@ -0,0 +1,47 @@ +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +def get_latest_entries(req: func.HttpRequest) -> func.HttpResponse: + database = DB() + time_entry_dao = _infrastructure.TimeEntriesSQLDao(database) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.GetLastestTimeEntryUseCase(time_entry_service) + + try: + owner_id = req.params.get("owner_id") + limit = req.params.get("limit") + + if not owner_id: + return func.HttpResponse( + body="No owner id found", + status_code=404, + mimetype="application/json" + ) + + time_entries = use_case.get_latest_entries(int(owner_id), int(limit) if limit and int(limit) > 0 else None) + + if not time_entries or len(time_entries) == 0: + return func.HttpResponse( + body="No time entries found", + status_code=404, + mimetype="application/json" + ) + + return func.HttpResponse( + body=json.dumps(time_entries, default=str), + status_code=200, + mimetype="application/json", + ) + + except ValueError: + return func.HttpResponse( + body=b"Invalid Format ID", + status_code=400, + mimetype="application/json" + ) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index a8b2081c..840fadb5 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -4,4 +4,5 @@ from ._services import TimeEntryService from ._use_cases import ( CreateTimeEntryUseCase, + GetLastestTimeEntryUseCase ) \ 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 5d04c861..2e5bd697 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,4 +1,5 @@ import abc +import typing from time_tracker.time_entries._domain import TimeEntry @@ -7,3 +8,7 @@ class TimeEntriesDao(abc.ABC): @abc.abstractmethod def create(self, time_entry_data: TimeEntry) -> TimeEntry: pass + + @abc.abstractmethod + def get_latest_entries(self, owner_id: int, limit: int) -> typing.List[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 d7aaf3ba..2f1117fa 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,5 @@ from time_tracker.time_entries._domain import TimeEntry, TimeEntriesDao +import typing class TimeEntryService: @@ -8,3 +9,6 @@ def __init__(self, time_entry_dao: TimeEntriesDao): def create(self, time_entry_data: TimeEntry) -> TimeEntry: return self.time_entry_dao.create(time_entry_data) + + def get_latest_entries(self, owner_id: int, limit: int) -> typing.List[TimeEntry]: + return self.time_entry_dao.get_latest_entries(owner_id, limit) 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 41aca738..7c9572ad 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa from ._create_time_entry_use_case import CreateTimeEntryUseCase +from ._get_latest_entries_use_case import GetLastestTimeEntryUseCase \ No newline at end of file diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_latest_entries_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_latest_entries_use_case.py new file mode 100644 index 00000000..c070f8ac --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_latest_entries_use_case.py @@ -0,0 +1,11 @@ +from time_tracker.time_entries._domain import TimeEntry, TimeEntryService +import typing + + +class GetLastestTimeEntryUseCase: + + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def get_latest_entries(self, owner_id: int, limit: int) -> typing.List[TimeEntry]: + return self.time_entry_service.get_latest_entries(owner_id, limit) 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 d233f3e9..c9c11f64 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 @@ -1,6 +1,6 @@ import dataclasses - import sqlalchemy +import typing import time_tracker.time_entries._domain as domain from time_tracker._infrastructure import _db @@ -47,3 +47,17 @@ def create(self, time_entry_data: domain.TimeEntry) -> domain.TimeEntry: def __create_time_entry_dto(self, time_entry: dict) -> domain.TimeEntry: time_entry = {key: time_entry.get(key) for key in self.time_entry_key} return domain.TimeEntry(**time_entry) + + def get_latest_entries(self, owner_id: int, limit: int = 20) -> typing.List[domain.TimeEntry]: + query = ( + self.time_entry.select() + .where(sqlalchemy.and_( + self.time_entry.c.owner_id == owner_id, + self.time_entry.c.deleted.is_(False) + )) + .order_by(self.time_entry.c.start_date.desc()) + .limit(limit) + ) + time_entries_data = self.db.get_session().execute(query) + list_time_entries = [dict(entry) for entry in time_entries_data] + return list_time_entries if len(list_time_entries) > 0 else None diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index d0182780..b5b83812 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 \ No newline at end of file +from ._application import create_time_entry +from ._application import get_latest_entries \ No newline at end of file