diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 36bff27a..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 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 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``` 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..dc025f68 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -76,3 +76,13 @@ functions: - POST route: activities/ authLevel: anonymous + + delete_time_entry: + handler: time_tracker/time_entries/interface.delete_time_entry + events: + - http: true + x-azure-settings: + methods: + - DELETE + route: time-entries/{id} + 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..dbe44176 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -1,37 +1,25 @@ -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 +ACTIVITY_URL = '/api/activities/' 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 +29,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 +50,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 +72,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 +94,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..a9623b3c --- /dev/null +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -0,0 +1,84 @@ +import pytest +import json + +import azure.functions as func + +import time_tracker.time_entries._application._time_entries as azure_time_entries +from time_tracker._infrastructure import DB +from time_tracker.time_entries import _domain as domain_time_entries +from time_tracker.time_entries import _infrastructure as infrastructure_time_entries + + +TIME_ENTRY_URL = "/api/time-entries/" + + +@pytest.fixture(name='insert_time_entry') +def _insert_time_entry() -> domain_time_entries.TimeEntry: + def _new_time_entry(time_entry: domain_time_entries.TimeEntry, database: DB): + dao = infrastructure_time_entries.TimeEntriesSQLDao(database) + new_time_entry = dao.create(time_entry) + return new_time_entry + 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 +): + 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 + + +def test__delete_time_entries_azure_endpoint__returns_an_time_entry_with_true_deleted__when_its_id_is_found( + test_db, time_entry_factory, insert_time_entry, insert_activity, activity_factory, +): + db = test_db + azure_time_entries._delete_time_entry.DATABASE = db + inserted_activity = insert_activity(activity_factory(), 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, db) + + req = func.HttpRequest( + method='DELETE', + body=None, + url=TIME_ENTRY_URL, + route_params={"id": inserted_time_entry.id}, + ) + + response = azure_time_entries._delete_time_entry.delete_time_entry(req) + time_entry_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == 200 + assert time_entry_json_data['deleted'] is True + + +def test__delete_time_entries_azure_endpoint__returns_a_status_code_400__when_time_entry_recive_invalid_id( + test_db, +): + db = test_db + azure_time_entries._delete_time_entry.DATABASE = db + + req = func.HttpRequest( + method="DELETE", + body=None, + url=TIME_ENTRY_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_time_entries._delete_time_entry.delete_time_entry(req) + + assert response.status_code == 400 + assert response.get_body() == b'Invalid Format ID' 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..8568bdb6 100644 --- a/V2/tests/fixtures.py +++ b/V2/tests/fixtures.py @@ -1,17 +1,21 @@ 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 + 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, @@ -19,17 +23,53 @@ def _make_activity( 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='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..86607479 --- /dev/null +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -0,0 +1,71 @@ +import pytest +from faker import Faker + +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 + + +def test_delete__returns_an_time_entry_with_true_deleted__when_an_time_entry_matching_its_id_is_found( + create_fake_dao, time_entry_factory, insert_activity, activity_factory +): + dao = create_fake_dao + inserted_activity = insert_activity(activity_factory(), dao.db) + existent_time_entry = time_entry_factory(activity_id=inserted_activity.id, technologies="[jira,sql]") + inserted_time_entry = dao.create(existent_time_entry) + + result = dao.delete(inserted_time_entry.id) + + assert result.deleted is True + + +def test_delete__returns_none__when_no_time_entry_matching_its_id_is_found( + create_fake_dao, +): + dao = create_fake_dao + + result = dao.delete(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 new file mode 100644 index 00000000..779eefa2 --- /dev/null +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -0,0 +1,31 @@ +from faker import Faker + +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 + + +def test__delete_time_entry__uses_the_time_entry_dao__to_change_time_entry_deleted( + mocker, +): + expected_time_entry = mocker.Mock() + time_entry_dao = mocker.Mock( + delete=mocker.Mock(return_value=expected_time_entry) + ) + + time_entry_service = TimeEntryService(time_entry_dao) + deleted_time_entry = time_entry_service.delete(Faker().pyint()) + + assert time_entry_dao.delete.called + assert expected_time_entry == deleted_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..d5095c4f --- /dev/null +++ b/V2/tests/unit/use_cases/time_entries_use_case_test.py @@ -0,0 +1,17 @@ +from pytest_mock import MockFixture +from faker import Faker + +from time_tracker.time_entries._domain import _use_cases + + +def test__delete_time_entry_function__uses_the_time_entry_service__to_change_time_entry_deleted( + mocker: MockFixture, +): + expected_time_entry = mocker.Mock() + time_entry_service = mocker.Mock(delete=mocker.Mock(return_value=expected_time_entry)) + + time_entry_use_case = _use_cases.DeleteTimeEntryUseCase(time_entry_service) + deleted_time_entry = time_entry_use_case.delete_time_entry(Faker().pyint()) + + assert time_entry_service.delete.called + assert expected_time_entry == deleted_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..338c6b00 100644 --- a/V2/time_tracker/_infrastructure/_db.py +++ b/V2/time_tracker/_infrastructure/_db.py @@ -2,6 +2,8 @@ from . import _config +_TEST_DIALECT = "sqlite" + class DB(): config = _config.load_config() @@ -14,7 +16,9 @@ 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() + if self.engine.dialect.name == _TEST_DIALECT: + self.connection.execute("pragma foreign_keys=ON") 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..2f7cebc5 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._time_entries import delete_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..7203d16d --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._delete_time_entry import delete_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..9d1ed223 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py @@ -0,0 +1,66 @@ +import dataclasses +import json +import typing + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure + +_JSON_PATH = ( + 'time_tracker/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() + + 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/_application/_time_entries/_delete_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py new file mode 100644 index 00000000..8a4be18c --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py @@ -0,0 +1,38 @@ +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + +DATABASE = DB() + + +def delete_time_entry(req: func.HttpRequest) -> func.HttpResponse: + time_entry_dao = _infrastructure.TimeEntriesSQLDao(DATABASE) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.DeleteTimeEntryUseCase(time_entry_service) + + try: + time_entry_id = int(req.route_params.get("id")) + deleted_time_entry = use_case.delete_time_entry(time_entry_id) + if not deleted_time_entry: + return func.HttpResponse( + body="Not found", + status_code=404, + mimetype="application/json" + ) + + return func.HttpResponse( + body=json.dumps(deleted_time_entry.__dict__, 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 new file mode 100644 index 00000000..3baaf4f0 --- /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 ( + DeleteTimeEntryUseCase +) \ 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..e7d94608 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -0,0 +1,13 @@ +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 + + @abc.abstractmethod + def delete(self, id: int) -> 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..fe29859e --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -0,0 +1,13 @@ +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) + + def delete(self, time_entry_id: int) -> TimeEntry: + return self.time_entry_dao.delete(time_entry_id) 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..62a40986 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._delete_time_entry_use_case import DeleteTimeEntryUseCase 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..73c32d81 --- /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.__dict__) diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_delete_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_delete_time_entry_use_case.py new file mode 100644 index 00000000..a195c303 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_delete_time_entry_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.time_entries._domain import TimeEntry, TimeEntryService + + +class DeleteTimeEntryUseCase: + + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def delete_time_entry(self, id: int) -> TimeEntry: + return self.time_entry_service.delete(id) 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..6037af9f --- /dev/null +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -0,0 +1,60 @@ +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) + + def delete(self, time_entry_id: int) -> domain.TimeEntry: + query = ( + self.time_entry.update() + .where(self.time_entry.c.id == time_entry_id) + .values({"deleted": True}) + ) + self.db.get_session().execute(query) + query_deleted_time_entry = sqlalchemy.sql.select(self.time_entry).where(self.time_entry.c.id == time_entry_id) + time_entry = self.db.get_session().execute(query_deleted_time_entry).one_or_none() + return self.__create_time_entry_dto(dict(time_entry)) if time_entry else None diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py new file mode 100644 index 00000000..22e1c166 --- /dev/null +++ b/V2/time_tracker/time_entries/interface.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._application import delete_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