From 8c7eb62745ea6f08d943ed05c9ce6fa53792a1fd Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 09:39:24 -0500 Subject: [PATCH 01/25] Prune placeholder code --- nad_ch/controllers/cli.py | 31 +++++-------------------------- nad_ch/entities.py | 13 ------------- nad_ch/use_cases.py | 15 ++------------- 3 files changed, 7 insertions(+), 52 deletions(-) diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index 5d4e841..60a1bd9 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -1,6 +1,5 @@ import click -from ..entities import File -from ..use_cases import upload_file, list_files, get_file_metadata +from ..use_cases import ingest_data_submission @click.group() @@ -11,28 +10,8 @@ def cli(ctx): @cli.command() @click.pass_context -@click.argument('filename') -@click.argument('content') -def upload(ctx, filename, content): +@click.argument('file_path') +def ingest(ctx, file_path): context = ctx.obj - file = File(name=filename, content=content) - upload_file(context, file) - click.echo(f"Uploaded {filename}") - - -@cli.command() -@click.pass_context -def listall(ctx): - context = ctx.obj - files = list_files(context) - for file in files: - click.echo(file.name) - - -@cli.command() -@click.pass_context -@click.argument('filename') -def metadata(ctx, filename): - context = ctx.obj - metadata = get_file_metadata(context, filename) - click.echo(f"File: {metadata.name}, Size: {metadata.size} bytes") + ingest_data_submission(context, file_path) + click.echo(f"Ingest complete") diff --git a/nad_ch/entities.py b/nad_ch/entities.py index db3a703..e69de29 100644 --- a/nad_ch/entities.py +++ b/nad_ch/entities.py @@ -1,13 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class File: - name: str - content: str - - -@dataclass -class FileMetadata: - name: str - size: int diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 16c6229..b33de34 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,18 +1,7 @@ from .application_context import ApplicationContext -from .entities import File, FileMetadata from .interfaces.storage import StorageGateway -def upload_file(ctx: ApplicationContext, file: File) -> None: +def ingest_data_submission(ctx: ApplicationContext, file_path: str) -> None: storage: StorageGateway = ctx.storage - storage.save(file) - - -def list_files(ctx: ApplicationContext) -> list[File]: - storage: StorageGateway = ctx.storage - return storage.list_all() - - -def get_file_metadata(ctx: ApplicationContext, file_name: str) -> FileMetadata: - storage: StorageGateway = ctx.storage - return storage.get_metadata(file_name) + storage.save(file_path) From 0b6b2030306459360cb9e1196617d3001dfc2d33 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 10:21:45 -0500 Subject: [PATCH 02/25] Prune tests --- nad_ch/controllers/cli.py | 9 +++++---- nad_ch/entities.py | 9 +++++++++ nad_ch/use_cases.py | 4 +++- tests/test_use_cases.py | 42 +++++++++++---------------------------- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index 60a1bd9..5f16627 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -10,8 +10,9 @@ def cli(ctx): @cli.command() @click.pass_context -@click.argument('file_path') -def ingest(ctx, file_path): +@click.argument('filepath') +@click.argument('provider') +def ingest(ctx, file_path, provider): context = ctx.obj - ingest_data_submission(context, file_path) - click.echo(f"Ingest complete") + ingest_data_submission(context, file_path, provider) + click.echo("Ingest complete") diff --git a/nad_ch/entities.py b/nad_ch/entities.py index e69de29..6d56a45 100644 --- a/nad_ch/entities.py +++ b/nad_ch/entities.py @@ -0,0 +1,9 @@ +class DataProvider: + def __init__(self, name: str): + self.name = name + + +class DataSubmission: + def __init__(self, file_path: str, provider: DataProvider): + self.file_path = file_path + self.provider = provider diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index b33de34..39acd48 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -2,6 +2,8 @@ from .interfaces.storage import StorageGateway -def ingest_data_submission(ctx: ApplicationContext, file_path: str) -> None: +def ingest_data_submission( + ctx: ApplicationContext, file_path: str, provider_name: str +) -> None: storage: StorageGateway = ctx.storage storage.save(file_path) diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index acf57a5..cdce6df 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -1,7 +1,7 @@ import pytest from nad_ch.application_context import create_app_context -from nad_ch.entities import File, FileMetadata -from nad_ch.use_cases import upload_file, list_files, get_file_metadata +from nad_ch.entities import DataProvider +from nad_ch.use_cases import ingest_data_submission @pytest.fixture(scope="function") @@ -10,33 +10,15 @@ def app_context(): yield context -def test_upload_file(app_context): - file = File(name="test.txt", content="Sample content") +def test_ingest_data_submission(app_context): + # Arrange + provider = DataProvider('State X') + app_context.providers.save(provider) - upload_file(app_context, file) + # Act + ingest_data_submission(app_context, 'some_file_path', provider.name) - stored_file = app_context.storage.get_file("test.txt") - assert stored_file.name == "test.txt" - assert stored_file.content == "Sample content" - - -def test_list_files(app_context): - file1 = File(name="test1.txt", content="Content 1") - file2 = File(name="test2.txt", content="Content 2") - upload_file(app_context, file1) - upload_file(app_context, file2) - - files = list_files(app_context) - - assert len(files) == 2 - assert any(f.name == "test1.txt" for f in files) - assert any(f.name == "test2.txt" for f in files) - - -def test_get_file_metadata(app_context): - file = File(name="test.txt", content="Sample content for metadata") - upload_file(app_context, file) - - metadata = get_file_metadata(app_context, "test.txt") - - assert isinstance(metadata, FileMetadata) + # Assert + submission = app_context.submissions.get_by_file_path("some_file_path") + assert submission.file_path == "some_file_path" + assert submission.provider.name == provider.name From ace0a489121a83bab8b8d6c7f76fe2481412cf6d Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 10:37:09 -0500 Subject: [PATCH 03/25] Rework directory structure, start on repositories --- nad_ch/gateways/storage_mock.py | 25 ------------------- .../{gateways => infrastructure}/__init__.py | 0 nad_ch/infrastructure/database.py | 6 +++++ .../__init__.py => infrastructure/storage.py} | 0 nad_ch/interfaces/storage.py | 13 ---------- nad_ch/repositories.py | 7 ++++++ 6 files changed, 13 insertions(+), 38 deletions(-) delete mode 100644 nad_ch/gateways/storage_mock.py rename nad_ch/{gateways => infrastructure}/__init__.py (100%) create mode 100644 nad_ch/infrastructure/database.py rename nad_ch/{interfaces/__init__.py => infrastructure/storage.py} (100%) delete mode 100644 nad_ch/interfaces/storage.py create mode 100644 nad_ch/repositories.py diff --git a/nad_ch/gateways/storage_mock.py b/nad_ch/gateways/storage_mock.py deleted file mode 100644 index 6260e7d..0000000 --- a/nad_ch/gateways/storage_mock.py +++ /dev/null @@ -1,25 +0,0 @@ -from ..entities import File, FileMetadata -from ..interfaces.storage import StorageGateway - - -class StorageGatewayMock(StorageGateway): - def __init__(self): - self.files = [] - - def save(self, file: File) -> None: - self.files.append(file) - - def list_all(self) -> list[File]: - return self.files - - def get_file(self, file_name: str) -> File: - file = next((f for f in self.files if f.name == file_name), None) - if not file: - raise ValueError(f"No file named {file_name} found!") - return file - - def get_metadata(self, file_name: str) -> FileMetadata: - file = next((f for f in self.files if f.name == file_name), None) - if not file: - raise ValueError(f"No file named {file_name} found!") - return FileMetadata(name=file.name, size=len(file.content)) diff --git a/nad_ch/gateways/__init__.py b/nad_ch/infrastructure/__init__.py similarity index 100% rename from nad_ch/gateways/__init__.py rename to nad_ch/infrastructure/__init__.py diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py new file mode 100644 index 0000000..1c55e7e --- /dev/null +++ b/nad_ch/infrastructure/database.py @@ -0,0 +1,6 @@ +from ..repositories import DataProviderRepository + + +class SqlAlchemyDataProviderRepostiory(DataProviderRepository): + def __init__(): + pass diff --git a/nad_ch/interfaces/__init__.py b/nad_ch/infrastructure/storage.py similarity index 100% rename from nad_ch/interfaces/__init__.py rename to nad_ch/infrastructure/storage.py diff --git a/nad_ch/interfaces/storage.py b/nad_ch/interfaces/storage.py deleted file mode 100644 index 3eddac3..0000000 --- a/nad_ch/interfaces/storage.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Protocol -from ..entities import File, FileMetadata - - -class StorageGateway(Protocol): - def save(self, file: File) -> None: - ... - - def list_all(self) -> list[File]: - ... - - def get_metadata(self, file_name: str) -> FileMetadata: - ... diff --git a/nad_ch/repositories.py b/nad_ch/repositories.py new file mode 100644 index 0000000..f4628ee --- /dev/null +++ b/nad_ch/repositories.py @@ -0,0 +1,7 @@ +from typing import Protocol +from .entities import DataProvider + + +def DataProviderRepository(Protocol): + def save(self, provider: DataProvider) -> None: + ... From 72579eeca1f7865759710db97a3f67061d7c3b5b Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 10:52:59 -0500 Subject: [PATCH 04/25] Add sqlalchemy --- nad_ch/infrastructure/database.py | 30 +++++- nad_ch/repositories.py | 4 +- nad_ch/use_cases.py | 4 +- poetry.lock | 173 +++++++++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 203 insertions(+), 9 deletions(-) diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 1c55e7e..9642300 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -1,6 +1,32 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.ext.declarative import declarative_base +from ..entities import DataProvider from ..repositories import DataProviderRepository +ModelBase = declarative_base() + + +class DataProviderModel(ModelBase): + __tablename__ = 'data_providers' + + id = Column(Integer, primary_key=True) + name = Column(String) + + @staticmethod + def from_entity(provider): + return DataProviderModel(name=provider.name) + + def to_entity(self): + return DataProvider(name=self.name) + + class SqlAlchemyDataProviderRepostiory(DataProviderRepository): - def __init__(): - pass + def __init__(self, session): + self.session = session + + def add(self, provider: DataProvider): + provider_model = DataProviderModel.from_entity(provider) + self.session.add(provider_model) + self.session.commit() + return provider_model.to_entity() diff --git a/nad_ch/repositories.py b/nad_ch/repositories.py index f4628ee..1a57831 100644 --- a/nad_ch/repositories.py +++ b/nad_ch/repositories.py @@ -2,6 +2,6 @@ from .entities import DataProvider -def DataProviderRepository(Protocol): - def save(self, provider: DataProvider) -> None: +class DataProviderRepository(Protocol): + def add(self, provider: DataProvider) -> None: ... diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 39acd48..e918805 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,9 +1,7 @@ from .application_context import ApplicationContext -from .interfaces.storage import StorageGateway def ingest_data_submission( ctx: ApplicationContext, file_path: str, provider_name: str ) -> None: - storage: StorageGateway = ctx.storage - storage.save(file_path) + pass diff --git a/poetry.lock b/poetry.lock index b79eecf..1392eea 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. [[package]] name = "click" @@ -105,6 +105,77 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.11.0,<2.12.0" pyflakes = ">=3.1.0,<3.2.0" +[[package]] +name = "greenlet" +version = "3.0.2" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9acd8fd67c248b8537953cb3af8787c18a87c33d4dcf6830e410ee1f95a63fd4"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:339c0272a62fac7e602e4e6ec32a64ff9abadc638b72f17f6713556ed011d493"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38878744926cec29b5cc3654ef47f3003f14bfbba7230e3c8492393fe29cc28b"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3f0497db77cfd034f829678b28267eeeeaf2fc21b3f5041600f7617139e6773"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1a8a08de7f68506a38f9a2ddb26bbd1480689e66d788fcd4b5f77e2d9ecfcc"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89a6f6ddcbef4000cda7e205c4c20d319488ff03db961d72d4e73519d2465309"}, + {file = "greenlet-3.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1f647fe5b94b51488b314c82fdda10a8756d650cee8d3cd29f657c6031bdf73"}, + {file = "greenlet-3.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9560c580c896030ff9c311c603aaf2282234643c90d1dec738a1d93e3e53cd51"}, + {file = "greenlet-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2e9c5423046eec21f6651268cb674dfba97280701e04ef23d312776377313206"}, + {file = "greenlet-3.0.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1fd25dfc5879a82103b3d9e43fa952e3026c221996ff4d32a9c72052544835d"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfdc950dd25f25d6582952e58521bca749cf3eeb7a9bad69237024308c8196"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edf7a1daba1f7c54326291a8cde58da86ab115b78c91d502be8744f0aa8e3ffa"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4cf532bf3c58a862196b06947b1b5cc55503884f9b63bf18582a75228d9950e"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e79fb5a9fb2d0bd3b6573784f5e5adabc0b0566ad3180a028af99523ce8f6138"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:006c1028ac0cfcc4e772980cfe73f5476041c8c91d15d64f52482fc571149d46"}, + {file = "greenlet-3.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fefd5eb2c0b1adffdf2802ff7df45bfe65988b15f6b972706a0e55d451bffaea"}, + {file = "greenlet-3.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c0fdb8142742ee68e97c106eb81e7d3e883cc739d9c5f2b28bc38a7bafeb6d1"}, + {file = "greenlet-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:8f8d14a0a4e8c670fbce633d8b9a1ee175673a695475acd838e372966845f764"}, + {file = "greenlet-3.0.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:654b84c9527182036747938b81938f1d03fb8321377510bc1854a9370418ab66"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bc4fde0842ff2b9cf33382ad0b4db91c2582db836793d58d174c569637144"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27b142a9080bdd5869a2fa7ebf407b3c0b24bd812db925de90e9afe3c417fd6"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0df7eed98ea23b20e9db64d46eb05671ba33147df9405330695bcd81a73bb0c9"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb5d60805057d8948065338be6320d35e26b0a72f45db392eb32b70dd6dc9227"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0e28f5233d64c693382f66d47c362b72089ebf8ac77df7e12ac705c9fa1163d"}, + {file = "greenlet-3.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e4bfa752b3688d74ab1186e2159779ff4867644d2b1ebf16db14281f0445377"}, + {file = "greenlet-3.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c42bb589e6e9f9d8bdd79f02f044dff020d30c1afa6e84c0b56d1ce8a324553c"}, + {file = "greenlet-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:b2cedf279ca38ef3f4ed0d013a6a84a7fc3d9495a716b84a5fc5ff448965f251"}, + {file = "greenlet-3.0.2-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:6d65bec56a7bc352bcf11b275b838df618651109074d455a772d3afe25390b7d"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0acadbc3f72cb0ee85070e8d36bd2a4673d2abd10731ee73c10222cf2dd4713c"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14b5d999aefe9ffd2049ad19079f733c3aaa426190ffecadb1d5feacef8fe397"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f27aa32466993c92d326df982c4acccd9530fe354e938d9e9deada563e71ce76"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f34a765c5170c0673eb747213a0275ecc749ab3652bdbec324621ed5b2edaef"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:520fcb53a39ef90f5021c77606952dbbc1da75d77114d69b8d7bded4a8e1a813"}, + {file = "greenlet-3.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1fceb5351ab1601903e714c3028b37f6ea722be6873f46e349a960156c05650"}, + {file = "greenlet-3.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7363756cc439a503505b67983237d1cc19139b66488263eb19f5719a32597836"}, + {file = "greenlet-3.0.2-cp37-cp37m-win32.whl", hash = "sha256:d5547b462b8099b84746461e882a3eb8a6e3f80be46cb6afb8524eeb191d1a30"}, + {file = "greenlet-3.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:950e21562818f9c771989b5b65f990e76f4ac27af66e1bb34634ae67886ede2a"}, + {file = "greenlet-3.0.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d64643317e76b4b41fdba659e7eca29634e5739b8bc394eda3a9127f697ed4b0"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f9ea7c2c9795549653b6f7569f6bc75d2c7d1f6b2854eb8ce0bc6ec3cb2dd88"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db4233358d3438369051a2f290f1311a360d25c49f255a6c5d10b5bcb3aa2b49"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bf77b41798e8417657245b9f3649314218a4a17aefb02bb3992862df32495"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d0df07a38e41a10dfb62c6fc75ede196572b580f48ee49b9282c65639f3965"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d247260db20887ae8857c0cbc750b9170f0b067dd7d38fb68a3f2334393bd3"}, + {file = "greenlet-3.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a37ae53cca36823597fd5f65341b6f7bac2dd69ecd6ca01334bb795460ab150b"}, + {file = "greenlet-3.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:80d068e4b6e2499847d916ef64176811ead6bf210a610859220d537d935ec6fd"}, + {file = "greenlet-3.0.2-cp38-cp38-win32.whl", hash = "sha256:b1405614692ac986490d10d3e1a05e9734f473750d4bee3cf7d1286ef7af7da6"}, + {file = "greenlet-3.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8756a94ed8f293450b0e91119eca2a36332deba69feb2f9ca410d35e74eae1e4"}, + {file = "greenlet-3.0.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:2c93cd03acb1499ee4de675e1a4ed8eaaa7227f7949dc55b37182047b006a7aa"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dac09e3c0b78265d2e6d3cbac2d7c48bd1aa4b04a8ffeda3adde9f1688df2c3"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ee59c4627c8c4bb3e15949fbcd499abd6b7f4ad9e0bfcb62c65c5e2cabe0ec4"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18fe39d70d482b22f0014e84947c5aaa7211fb8e13dc4cc1c43ed2aa1db06d9a"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84bef3cfb6b6bfe258c98c519811c240dbc5b33a523a14933a252e486797c90"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aecea0442975741e7d69daff9b13c83caff8c13eeb17485afa65f6360a045765"}, + {file = "greenlet-3.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f260e6c2337871a52161824058923df2bbddb38bc11a5cbe71f3474d877c5bd9"}, + {file = "greenlet-3.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc14dd9554f88c9c1fe04771589ae24db76cd56c8f1104e4381b383d6b71aff8"}, + {file = "greenlet-3.0.2-cp39-cp39-win32.whl", hash = "sha256:bfcecc984d60b20ffe30173b03bfe9ba6cb671b0be1e95c3e2056d4fe7006590"}, + {file = "greenlet-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:c235131bf59d2546bb3ebaa8d436126267392f2e51b85ff45ac60f3a26549af0"}, + {file = "greenlet-3.0.2.tar.gz", hash = "sha256:1c1129bc47266d83444c85a8e990ae22688cf05fb20d7951fd2866007c2ba9bc"}, +] + +[package.extras] +docs = ["Sphinx"] +test = ["objgraph", "psutil"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -195,7 +266,105 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "sqlalchemy" +version = "2.0.23" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, + {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, + {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.2.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "891c7e1e01e9f7ed8ae76d9e455c3ba2966d80509e49604c396b49301321cb2c" +content-hash = "737de6a814d1ac848ead087c85ee7b300fdc6c1542239357187a0e12d71576c8" diff --git a/pyproject.toml b/pyproject.toml index 2758f62..567c311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ authors = [] [tool.poetry.dependencies] python = "^3.11" click = "^8.1.7" +sqlalchemy = "^2.0.23" [tool.poetry.group.dev.dependencies] pytest = "^7.4.2" From 87a8ab04fdffa35d498465d3855aede84850570e Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 12:14:36 -0500 Subject: [PATCH 05/25] First basic use case --- nad_ch/application_context.py | 11 ++++++----- nad_ch/controllers/cli.py | 2 +- nad_ch/infrastructure/database.py | 10 +++++++++- nad_ch/repositories.py | 3 +++ nad_ch/use_cases.py | 9 ++++++++- poetry.lock | 25 +++++++++++++++++++---- pyproject.toml | 6 ++++++ tests/mocks.py | 13 ++++++++++++ tests/test_use_cases.py | 33 ++++++++++++++++++++----------- 9 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 tests/mocks.py diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 4b4da37..a5e4009 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -1,19 +1,20 @@ import os -from .gateways.storage_mock import StorageGatewayMock +from .infrastructure.database import SqlAlchemyDataProviderRepostiory +from tests.mocks import MockDataProviderRepository class ApplicationContext: def __init__(self): - self._storage = StorageGatewayMock() + self._providers = SqlAlchemyDataProviderRepostiory() @property - def storage(self): - return self._storage + def providers(self): + return self._providers class TestApplicationContext(ApplicationContext): def __init__(self): - self._storage = StorageGatewayMock() + self._providers = MockDataProviderRepository() def create_app_context(): diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index 5f16627..0bb537c 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -15,4 +15,4 @@ def cli(ctx): def ingest(ctx, file_path, provider): context = ctx.obj ingest_data_submission(context, file_path, provider) - click.echo("Ingest complete") + click.echo('Ingest complete') diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 9642300..66f06ee 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -1,5 +1,5 @@ from sqlalchemy import Column, Integer, String -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base from ..entities import DataProvider from ..repositories import DataProviderRepository @@ -30,3 +30,11 @@ def add(self, provider: DataProvider): self.session.add(provider_model) self.session.commit() return provider_model.to_entity() + + def get_by_name(self, name: str) -> DataProvider: + provider_model = ( + self.session.query(DataProviderModel) + .filter(DataProviderModel.name == name) + .first() + ) + return provider_model.to_entity() diff --git a/nad_ch/repositories.py b/nad_ch/repositories.py index 1a57831..0bb924b 100644 --- a/nad_ch/repositories.py +++ b/nad_ch/repositories.py @@ -5,3 +5,6 @@ class DataProviderRepository(Protocol): def add(self, provider: DataProvider) -> None: ... + + def get_by_name(self, name: str) -> DataProvider: + ... diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index e918805..71e9b55 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,7 +1,14 @@ from .application_context import ApplicationContext +from .entities import DataProvider + +def add_data_provider( + ctx: ApplicationContext, provider_name: str +) -> None: + provider = DataProvider(provider_name) + ctx.providers.add(provider) def ingest_data_submission( ctx: ApplicationContext, file_path: str, provider_name: str ) -> None: - pass + pass diff --git a/poetry.lock b/poetry.lock index 1392eea..e57ccc7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -248,13 +248,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.2" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -266,6 +266,23 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-env" +version = "1.1.3" +description = "pytest plugin that allows you to add environment variables." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc"}, + {file = "pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"}, +] + +[package.dependencies] +pytest = ">=7.4.3" + +[package.extras] +test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] + [[package]] name = "sqlalchemy" version = "2.0.23" @@ -367,4 +384,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "737de6a814d1ac848ead087c85ee7b300fdc6c1542239357187a0e12d71576c8" +content-hash = "835cdf856d39ff8ddda6b3b3f6379fec611cbdc96bca255691f9c1e4d7520136" diff --git a/pyproject.toml b/pyproject.toml index 567c311..3455599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ authors = [] python = "^3.11" click = "^8.1.7" sqlalchemy = "^2.0.23" +pytest-env = "^1.1.3" [tool.poetry.group.dev.dependencies] pytest = "^7.4.2" @@ -21,3 +22,8 @@ coverage = "^7.3.2" [tool.poetry.scripts] start = "nad_ch.main:main" test = "pytest:main" + +[tool.pytest.ini_options] +env = [ + "APP_ENV=test", +] diff --git a/tests/mocks.py b/tests/mocks.py new file mode 100644 index 0000000..6fe3add --- /dev/null +++ b/tests/mocks.py @@ -0,0 +1,13 @@ +from nad_ch.entities import DataProvider +from nad_ch.repositories import DataProviderRepository + + +class MockDataProviderRepository(DataProviderRepository): + def __init__(self) -> None: + self._providers = set() + + def add(self, provider: DataProvider) -> None: + self._providers.add(provider) + + def get_by_name(self, name: str) -> DataProvider: + return next(p for p in self._providers if p.name == name) diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index cdce6df..56eaf31 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -1,24 +1,33 @@ import pytest from nad_ch.application_context import create_app_context from nad_ch.entities import DataProvider -from nad_ch.use_cases import ingest_data_submission +from nad_ch.use_cases import add_data_provider, ingest_data_submission -@pytest.fixture(scope="function") +@pytest.fixture(scope='function') def app_context(): context = create_app_context() yield context -def test_ingest_data_submission(app_context): - # Arrange - provider = DataProvider('State X') - app_context.providers.save(provider) +def test_add_data_provider(app_context): + name = 'State X' + add_data_provider(app_context, name) - # Act - ingest_data_submission(app_context, 'some_file_path', provider.name) + provider = app_context.providers.get_by_name(name) + assert provider.name == name + assert isinstance(provider, DataProvider) == True - # Assert - submission = app_context.submissions.get_by_file_path("some_file_path") - assert submission.file_path == "some_file_path" - assert submission.provider.name == provider.name + +# def test_ingest_data_submission(app_context): +# # Arrange +# provider = DataProvider('State X') +# app_context.providers.add(provider) + +# # Act +# ingest_data_submission(app_context, 'some_file_path', provider.name) + +# # Assert +# submission = app_context.submissions.get_by_file_path('some_file_path') +# assert submission.file_path == 'some_file_path' +# assert submission.provider.name == provider.name From e8c0ec40d08b5acf4436b6ab7a9664c46aec5efc Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 12:18:20 -0500 Subject: [PATCH 06/25] Fix linter errors --- nad_ch/use_cases.py | 1 + tests/test_use_cases.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 71e9b55..82cb60d 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,6 +1,7 @@ from .application_context import ApplicationContext from .entities import DataProvider + def add_data_provider( ctx: ApplicationContext, provider_name: str ) -> None: diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index 56eaf31..5e4c19b 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -16,7 +16,7 @@ def test_add_data_provider(app_context): provider = app_context.providers.get_by_name(name) assert provider.name == name - assert isinstance(provider, DataProvider) == True + assert isinstance(provider, DataProvider) is True # def test_ingest_data_submission(app_context): From e136ca4ae09d4bb130dbe82f20a8b323cfcb02a5 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 13:43:14 -0500 Subject: [PATCH 07/25] Basic provider repository test case --- .gitignore | 3 ++ nad_ch/application_context.py | 4 +-- nad_ch/config.py | 8 +++++ nad_ch/infrastructure/database.py | 51 +++++++++++++++++++-------- poetry.lock | 16 ++++++++- pyproject.toml | 2 ++ tests/infrastructure/__init__.py | 0 tests/infrastructure/test_database.py | 44 +++++++++++++++++++++++ 8 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 nad_ch/config.py create mode 100644 tests/infrastructure/__init__.py create mode 100644 tests/infrastructure/test_database.py diff --git a/.gitignore b/.gitignore index 68bc17f..f15b188 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Development databases +*.sqlite3 \ No newline at end of file diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index a5e4009..4ff6eab 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -1,11 +1,11 @@ import os -from .infrastructure.database import SqlAlchemyDataProviderRepostiory +from .infrastructure.database import session_scope, SqlAlchemyDataProviderRepository from tests.mocks import MockDataProviderRepository class ApplicationContext: def __init__(self): - self._providers = SqlAlchemyDataProviderRepostiory() + self._providers = SqlAlchemyDataProviderRepository(session_scope) @property def providers(self): diff --git a/nad_ch/config.py b/nad_ch/config.py new file mode 100644 index 0000000..3053bd9 --- /dev/null +++ b/nad_ch/config.py @@ -0,0 +1,8 @@ +from dotenv import load_dotenv +import os + + +load_dotenv() + + +DATABASE_URL = os.getenv('DATABASE_URL') diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 66f06ee..7b1fa36 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -1,9 +1,28 @@ -from sqlalchemy import Column, Integer, String -from sqlalchemy.orm import declarative_base +from sqlalchemy import Column, Integer, String, create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +import contextlib +from ..config import DATABASE_URL from ..entities import DataProvider from ..repositories import DataProviderRepository +engine = create_engine(DATABASE_URL) +Session = sessionmaker(bind=engine) + + +@contextlib.contextmanager +def session_scope(): + session = Session() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + ModelBase = declarative_base() @@ -21,20 +40,22 @@ def to_entity(self): return DataProvider(name=self.name) -class SqlAlchemyDataProviderRepostiory(DataProviderRepository): - def __init__(self, session): - self.session = session +class SqlAlchemyDataProviderRepository(DataProviderRepository): + def __init__(self, session_factory): + self.session_factory = session_factory def add(self, provider: DataProvider): - provider_model = DataProviderModel.from_entity(provider) - self.session.add(provider_model) - self.session.commit() - return provider_model.to_entity() + with self.session_factory() as session: + provider_model = DataProviderModel.from_entity(provider) + session.add(provider_model) + # session.commit() + return provider_model.to_entity() def get_by_name(self, name: str) -> DataProvider: - provider_model = ( - self.session.query(DataProviderModel) - .filter(DataProviderModel.name == name) - .first() - ) - return provider_model.to_entity() + with self.session_factory() as session: + provider_model = ( + session.query(DataProviderModel) + .filter(DataProviderModel.name == name) + .first() + ) + return provider_model.to_entity() diff --git a/poetry.lock b/poetry.lock index e57ccc7..775bf79 100644 --- a/poetry.lock +++ b/poetry.lock @@ -283,6 +283,20 @@ pytest = ">=7.4.3" [package.extras] test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] +[[package]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "sqlalchemy" version = "2.0.23" @@ -384,4 +398,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "835cdf856d39ff8ddda6b3b3f6379fec611cbdc96bca255691f9c1e4d7520136" +content-hash = "25f51c569e7fc868fa1b8d90964aee5ee4a27f52b164fd1a614f92bdfa699a96" diff --git a/pyproject.toml b/pyproject.toml index 3455599..2e1eef4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ pytest-env = "^1.1.3" pytest = "^7.4.2" flake8 = "^6.1.0" coverage = "^7.3.2" +python-dotenv = "^1.0.0" [tool.poetry.scripts] start = "nad_ch.main:main" @@ -26,4 +27,5 @@ test = "pytest:main" [tool.pytest.ini_options] env = [ "APP_ENV=test", + "DATABASE_URL=sqlite:///:memory:" ] diff --git a/tests/infrastructure/__init__.py b/tests/infrastructure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/infrastructure/test_database.py b/tests/infrastructure/test_database.py new file mode 100644 index 0000000..7257b7e --- /dev/null +++ b/tests/infrastructure/test_database.py @@ -0,0 +1,44 @@ +import pytest +import contextlib +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from nad_ch.config import DATABASE_URL +from nad_ch.entities import DataProvider +from nad_ch.infrastructure.database import ModelBase, SqlAlchemyDataProviderRepository + + +@pytest.fixture(scope='function') +def test_session(): + engine = create_engine(DATABASE_URL) + ModelBase.metadata.create_all(engine) + Session = sessionmaker(bind=engine) + + @contextlib.contextmanager + def test_session_scope(): + session = Session() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + return test_session_scope + + +@pytest.fixture(scope='function') +def providers(test_session): + return SqlAlchemyDataProviderRepository(test_session) + + +def test_add_data_provider_to_repository_and_get_by_name(providers): + provider_name = 'State X' + new_provider = DataProvider(provider_name) + + providers.add(new_provider) + + retreived_provider = providers.get_by_name(provider_name) + assert retreived_provider.name == provider_name + assert isinstance(retreived_provider, DataProvider) is True From 50893b304264908b08c844c023ac7d259c039986 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 13:52:35 -0500 Subject: [PATCH 08/25] Add script to init db --- scripts/init_db.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 scripts/init_db.py diff --git a/scripts/init_db.py b/scripts/init_db.py new file mode 100644 index 0000000..26bbdab --- /dev/null +++ b/scripts/init_db.py @@ -0,0 +1,20 @@ +import os +from sqlalchemy import create_engine +from nad_ch.infrastructure.database import ModelBase, DataProviderModel +from nad_ch.config import DATABASE_URL + + +def init_db(): + engine = create_engine(DATABASE_URL) + + # Check if the database file already exists + if os.path.exists(DATABASE_URL.split('///')[1]): + print('Database already exists.') + else: + # Create all tables + ModelBase.metadata.create_all(engine) + print('Database initialized and tables created.') + + +if __name__ == '__main__': + init_db() From 30a5bc1478bc5d036369a0245d4f8e13e38bfcbe Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 14:12:24 -0500 Subject: [PATCH 09/25] get all providers --- nad_ch/application_context.py | 2 +- nad_ch/controllers/cli.py | 21 ++++++++++++++++++++- nad_ch/infrastructure/database.py | 12 +++++++++--- nad_ch/main.py | 4 ++-- nad_ch/repositories.py | 7 +++++-- nad_ch/use_cases.py | 11 +++++++++-- tests/mocks.py | 3 +++ 7 files changed, 49 insertions(+), 11 deletions(-) diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 4ff6eab..1ac165b 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -1,5 +1,5 @@ import os -from .infrastructure.database import session_scope, SqlAlchemyDataProviderRepository +from infrastructure.database import session_scope, SqlAlchemyDataProviderRepository from tests.mocks import MockDataProviderRepository diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index 0bb537c..88531f6 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -1,5 +1,5 @@ import click -from ..use_cases import ingest_data_submission +from use_cases import add_data_provider, list_data_providers, ingest_data_submission @click.group() @@ -8,6 +8,25 @@ def cli(ctx): pass +@cli.command() +@click.pass_context +@click.argument('provider_name') +def add_provider(ctx, provider_name): + context = ctx.obj + add_data_provider(context, provider_name) + click.echo('Provider added') + + +@cli.command() +@click.pass_context +def list_providers(ctx): + context = ctx.obj + providers = list_data_providers(context) + click.echo('Data Provider Names:') + for p in providers: + click.echo(p.name) + + @cli.command() @click.pass_context @click.argument('filepath') diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 7b1fa36..8ecaccc 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -1,9 +1,9 @@ from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.orm import sessionmaker, declarative_base import contextlib -from ..config import DATABASE_URL -from ..entities import DataProvider -from ..repositories import DataProviderRepository +from config import DATABASE_URL +from entities import DataProvider +from repositories import DataProviderRepository engine = create_engine(DATABASE_URL) @@ -59,3 +59,9 @@ def get_by_name(self, name: str) -> DataProvider: .first() ) return provider_model.to_entity() + + def get_all(self): + with self.session_factory() as session: + provider_models = session.query(DataProviderModel).all() + providers_entities = [provider.to_entity() for provider in provider_models] + return providers_entities diff --git a/nad_ch/main.py b/nad_ch/main.py index 01d8463..620ed82 100644 --- a/nad_ch/main.py +++ b/nad_ch/main.py @@ -1,5 +1,5 @@ -from .controllers.cli import cli -from .application_context import create_app_context +from controllers.cli import cli +from application_context import create_app_context def main(): diff --git a/nad_ch/repositories.py b/nad_ch/repositories.py index 0bb924b..4fa534a 100644 --- a/nad_ch/repositories.py +++ b/nad_ch/repositories.py @@ -1,5 +1,5 @@ -from typing import Protocol -from .entities import DataProvider +from typing import List, Protocol +from entities import DataProvider class DataProviderRepository(Protocol): @@ -8,3 +8,6 @@ def add(self, provider: DataProvider) -> None: def get_by_name(self, name: str) -> DataProvider: ... + + def get_all(self) -> List[DataProvider]: + ... diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 82cb60d..5253952 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,5 +1,5 @@ -from .application_context import ApplicationContext -from .entities import DataProvider +from application_context import ApplicationContext +from entities import DataProvider def add_data_provider( @@ -9,6 +9,13 @@ def add_data_provider( ctx.providers.add(provider) +def list_data_providers( + ctx: ApplicationContext +): + list = ctx.providers.get_all() + return list + + def ingest_data_submission( ctx: ApplicationContext, file_path: str, provider_name: str ) -> None: diff --git a/tests/mocks.py b/tests/mocks.py index 6fe3add..5c5dfba 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -11,3 +11,6 @@ def add(self, provider: DataProvider) -> None: def get_by_name(self, name: str) -> DataProvider: return next(p for p in self._providers if p.name == name) + + def get_all(self): + return list(self._providers) From 196e2d6cef2bc11c7b692476fdebb97ddc688930 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 14:15:25 -0500 Subject: [PATCH 10/25] Some cleanup --- nad_ch/infrastructure/database.py | 3 ++- scripts/init_db.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 8ecaccc..6a3a742 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -1,3 +1,4 @@ +from typing import List from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.orm import sessionmaker, declarative_base import contextlib @@ -60,7 +61,7 @@ def get_by_name(self, name: str) -> DataProvider: ) return provider_model.to_entity() - def get_all(self): + def get_all(self) -> List[DataProvider]: with self.session_factory() as session: provider_models = session.query(DataProviderModel).all() providers_entities = [provider.to_entity() for provider in provider_models] diff --git a/scripts/init_db.py b/scripts/init_db.py index 26bbdab..bd7b49c 100644 --- a/scripts/init_db.py +++ b/scripts/init_db.py @@ -1,6 +1,6 @@ import os from sqlalchemy import create_engine -from nad_ch.infrastructure.database import ModelBase, DataProviderModel +from nad_ch.infrastructure.database import ModelBase from nad_ch.config import DATABASE_URL From 0be50016d6888a3dfb1e4c5cd9f031b2a22c6c26 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Dec 2023 14:41:40 -0500 Subject: [PATCH 11/25] Fix imports --- nad_ch/application_context.py | 2 +- nad_ch/controllers/cli.py | 2 +- nad_ch/infrastructure/database.py | 6 +++--- nad_ch/main.py | 4 ++-- nad_ch/repositories.py | 2 +- nad_ch/use_cases.py | 4 ++-- tests/test_use_cases.py | 12 +++++++++++- 7 files changed, 21 insertions(+), 11 deletions(-) diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 1ac165b..84e09e7 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -1,5 +1,5 @@ import os -from infrastructure.database import session_scope, SqlAlchemyDataProviderRepository +from nad_ch.infrastructure.database import session_scope, SqlAlchemyDataProviderRepository from tests.mocks import MockDataProviderRepository diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index 88531f6..1d05927 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -1,5 +1,5 @@ import click -from use_cases import add_data_provider, list_data_providers, ingest_data_submission +from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission @click.group() diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 6a3a742..2a90e0c 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -2,9 +2,9 @@ from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.orm import sessionmaker, declarative_base import contextlib -from config import DATABASE_URL -from entities import DataProvider -from repositories import DataProviderRepository +from nad_ch.config import DATABASE_URL +from nad_ch.entities import DataProvider +from nad_ch.repositories import DataProviderRepository engine = create_engine(DATABASE_URL) diff --git a/nad_ch/main.py b/nad_ch/main.py index 620ed82..10f4aca 100644 --- a/nad_ch/main.py +++ b/nad_ch/main.py @@ -1,5 +1,5 @@ -from controllers.cli import cli -from application_context import create_app_context +from nad_ch.controllers.cli import cli +from nad_ch.application_context import create_app_context def main(): diff --git a/nad_ch/repositories.py b/nad_ch/repositories.py index 4fa534a..3d6d6f7 100644 --- a/nad_ch/repositories.py +++ b/nad_ch/repositories.py @@ -1,5 +1,5 @@ from typing import List, Protocol -from entities import DataProvider +from nad_ch.entities import DataProvider class DataProviderRepository(Protocol): diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 5253952..4d1cd8c 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,5 +1,5 @@ -from application_context import ApplicationContext -from entities import DataProvider +from nad_ch.application_context import ApplicationContext +from nad_ch.entities import DataProvider def add_data_provider( diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index 5e4c19b..079046d 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -1,7 +1,7 @@ import pytest from nad_ch.application_context import create_app_context from nad_ch.entities import DataProvider -from nad_ch.use_cases import add_data_provider, ingest_data_submission +from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission @pytest.fixture(scope='function') @@ -19,6 +19,16 @@ def test_add_data_provider(app_context): assert isinstance(provider, DataProvider) is True +def test_list_data_providers(app_context): + name = 'State X' + add_data_provider(app_context, name) + + providers = list_data_providers(app_context) + + assert len(providers) == 1 + assert providers[0].name == name + + # def test_ingest_data_submission(app_context): # # Arrange # provider = DataProvider('State X') From 6512fbb6068ad7d7e9319cf12fd9559f58780fce Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 09:57:05 -0500 Subject: [PATCH 12/25] Handle exception in add provider use case --- nad_ch/controllers/cli.py | 9 +++++++-- nad_ch/use_cases.py | 9 +++++++++ tests/test_use_cases.py | 7 ++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index 1d05927..adfd4b9 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -1,5 +1,5 @@ import click -from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission +from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission, InvalidProviderNameException @click.group() @@ -13,7 +13,12 @@ def cli(ctx): @click.argument('provider_name') def add_provider(ctx, provider_name): context = ctx.obj - add_data_provider(context, provider_name) + try: + add_data_provider(context, provider_name) + except InvalidProviderNameException as e: + click.echo(f"Error: {e.message}") + return + click.echo('Provider added') diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 4d1cd8c..ef70bfd 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -5,6 +5,9 @@ def add_data_provider( ctx: ApplicationContext, provider_name: str ) -> None: + if not provider_name: + raise InvalidProviderNameException() + provider = DataProvider(provider_name) ctx.providers.add(provider) @@ -20,3 +23,9 @@ def ingest_data_submission( ctx: ApplicationContext, file_path: str, provider_name: str ) -> None: pass + + +class InvalidProviderNameException(Exception): + def __init__(self, message='Provider name is required'): + self.message = message + super().__init__(self.message) diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index 079046d..e09eb23 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -1,7 +1,7 @@ import pytest from nad_ch.application_context import create_app_context from nad_ch.entities import DataProvider -from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission +from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission, InvalidProviderNameException @pytest.fixture(scope='function') @@ -19,6 +19,11 @@ def test_add_data_provider(app_context): assert isinstance(provider, DataProvider) is True +def test_add_data_provider_throws_error_if_no_provider_name_given(app_context): + with pytest.raises(InvalidProviderNameException): + add_data_provider(app_context, '') + + def test_list_data_providers(app_context): name = 'State X' add_data_provider(app_context, name) From 26dca5ae8885771a2f30df456a7597f76deee16d Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 09:58:22 -0500 Subject: [PATCH 13/25] Fix linter issues --- nad_ch/application_context.py | 5 ++++- nad_ch/controllers/cli.py | 7 ++++++- tests/test_use_cases.py | 7 ++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 84e09e7..72d0698 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -1,5 +1,8 @@ import os -from nad_ch.infrastructure.database import session_scope, SqlAlchemyDataProviderRepository +from nad_ch.infrastructure.database import ( + session_scope, + SqlAlchemyDataProviderRepository +) from tests.mocks import MockDataProviderRepository diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index adfd4b9..9152e01 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -1,5 +1,10 @@ import click -from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission, InvalidProviderNameException +from nad_ch.use_cases import ( + add_data_provider, + list_data_providers, + ingest_data_submission, + InvalidProviderNameException +) @click.group() diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index e09eb23..1e8aee4 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -1,7 +1,12 @@ import pytest from nad_ch.application_context import create_app_context from nad_ch.entities import DataProvider -from nad_ch.use_cases import add_data_provider, list_data_providers, ingest_data_submission, InvalidProviderNameException +from nad_ch.use_cases import ( + add_data_provider, + list_data_providers, + ingest_data_submission, + InvalidProviderNameException +) @pytest.fixture(scope='function') From aeaba5740e24b1f92de9963b52b6f1bf56b7058b Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 10:15:13 -0500 Subject: [PATCH 14/25] Remove dead code --- nad_ch/infrastructure/database.py | 1 - tests/test_use_cases.py | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 2a90e0c..7f8ecc9 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -49,7 +49,6 @@ def add(self, provider: DataProvider): with self.session_factory() as session: provider_model = DataProviderModel.from_entity(provider) session.add(provider_model) - # session.commit() return provider_model.to_entity() def get_by_name(self, name: str) -> DataProvider: diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index 1e8aee4..edb12ae 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -4,7 +4,6 @@ from nad_ch.use_cases import ( add_data_provider, list_data_providers, - ingest_data_submission, InvalidProviderNameException ) @@ -37,17 +36,3 @@ def test_list_data_providers(app_context): assert len(providers) == 1 assert providers[0].name == name - - -# def test_ingest_data_submission(app_context): -# # Arrange -# provider = DataProvider('State X') -# app_context.providers.add(provider) - -# # Act -# ingest_data_submission(app_context, 'some_file_path', provider.name) - -# # Assert -# submission = app_context.submissions.get_by_file_path('some_file_path') -# assert submission.file_path == 'some_file_path' -# assert submission.provider.name == provider.name From 32e3a96387d673e88b2d126e8df157c31e177d6c Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 11:17:06 -0500 Subject: [PATCH 15/25] Add property accessor to test app context --- nad_ch/application_context.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 72d0698..68197ad 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -19,6 +19,10 @@ class TestApplicationContext(ApplicationContext): def __init__(self): self._providers = MockDataProviderRepository() + @property + def providers(self): + return self._providers + def create_app_context(): if os.environ.get('APP_ENV') == 'test': From c7f6a38200d9f30689cb8640e156bdc87a70efa2 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 11:19:13 -0500 Subject: [PATCH 16/25] Tweak directory structure --- nad_ch/{ => domain}/entities.py | 0 nad_ch/{ => domain}/repositories.py | 2 +- nad_ch/infrastructure/database.py | 4 ++-- nad_ch/use_cases.py | 2 +- tests/infrastructure/test_database.py | 2 +- tests/mocks.py | 4 ++-- tests/test_use_cases.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename nad_ch/{ => domain}/entities.py (100%) rename nad_ch/{ => domain}/repositories.py (84%) diff --git a/nad_ch/entities.py b/nad_ch/domain/entities.py similarity index 100% rename from nad_ch/entities.py rename to nad_ch/domain/entities.py diff --git a/nad_ch/repositories.py b/nad_ch/domain/repositories.py similarity index 84% rename from nad_ch/repositories.py rename to nad_ch/domain/repositories.py index 3d6d6f7..d9ace1a 100644 --- a/nad_ch/repositories.py +++ b/nad_ch/domain/repositories.py @@ -1,5 +1,5 @@ from typing import List, Protocol -from nad_ch.entities import DataProvider +from nad_ch.domain.entities import DataProvider class DataProviderRepository(Protocol): diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 7f8ecc9..6a1a130 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -3,8 +3,8 @@ from sqlalchemy.orm import sessionmaker, declarative_base import contextlib from nad_ch.config import DATABASE_URL -from nad_ch.entities import DataProvider -from nad_ch.repositories import DataProviderRepository +from nad_ch.domain.entities import DataProvider +from nad_ch.domain.repositories import DataProviderRepository engine = create_engine(DATABASE_URL) diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index ef70bfd..560a13a 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,5 +1,5 @@ from nad_ch.application_context import ApplicationContext -from nad_ch.entities import DataProvider +from nad_ch.domain.entities import DataProvider def add_data_provider( diff --git a/tests/infrastructure/test_database.py b/tests/infrastructure/test_database.py index 7257b7e..f487291 100644 --- a/tests/infrastructure/test_database.py +++ b/tests/infrastructure/test_database.py @@ -3,7 +3,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from nad_ch.config import DATABASE_URL -from nad_ch.entities import DataProvider +from nad_ch.domain.entities import DataProvider from nad_ch.infrastructure.database import ModelBase, SqlAlchemyDataProviderRepository diff --git a/tests/mocks.py b/tests/mocks.py index 5c5dfba..dc24205 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,5 +1,5 @@ -from nad_ch.entities import DataProvider -from nad_ch.repositories import DataProviderRepository +from nad_ch.domain.entities import DataProvider +from nad_ch.domain.repositories import DataProviderRepository class MockDataProviderRepository(DataProviderRepository): diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index edb12ae..3e87c5c 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -1,6 +1,6 @@ import pytest from nad_ch.application_context import create_app_context -from nad_ch.entities import DataProvider +from nad_ch.domain.entities import DataProvider from nad_ch.use_cases import ( add_data_provider, list_data_providers, From 8191cdd2d40c9025800eb4957162b891ffbd4a57 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 11:37:01 -0500 Subject: [PATCH 17/25] Add lint script --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2e1eef4..293db94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ coverage = "^7.3.2" python-dotenv = "^1.0.0" [tool.poetry.scripts] +lint = "flake8.main.cli:main" start = "nad_ch.main:main" test = "pytest:main" From 476c8a73c6782cf78de848afc3f07cd41f5b9d4a Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 11:40:09 -0500 Subject: [PATCH 18/25] Tweak db init script --- pyproject.toml | 1 + scripts/init_db.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 293db94..6d6becb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ coverage = "^7.3.2" python-dotenv = "^1.0.0" [tool.poetry.scripts] +init-db="scripts.init_db:main" lint = "flake8.main.cli:main" start = "nad_ch.main:main" test = "pytest:main" diff --git a/scripts/init_db.py b/scripts/init_db.py index bd7b49c..3573c7f 100644 --- a/scripts/init_db.py +++ b/scripts/init_db.py @@ -4,7 +4,7 @@ from nad_ch.config import DATABASE_URL -def init_db(): +def main(): engine = create_engine(DATABASE_URL) # Check if the database file already exists @@ -17,4 +17,4 @@ def init_db(): if __name__ == '__main__': - init_db() + main() From ec24b17f4468d985205b776b4039ad99a9edd6a7 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 16:02:12 -0500 Subject: [PATCH 19/25] Add adr for sqlalchemy usage --- .../adr/0003-use-sqlalchemy-as-orm.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 documentation/adr/0003-use-sqlalchemy-as-orm.md diff --git a/documentation/adr/0003-use-sqlalchemy-as-orm.md b/documentation/adr/0003-use-sqlalchemy-as-orm.md new file mode 100644 index 0000000..c85b58e --- /dev/null +++ b/documentation/adr/0003-use-sqlalchemy-as-orm.md @@ -0,0 +1,21 @@ +# 2. Use SQLAlchemy as ORM + +Date: 2023-12-13 + +## Status + +Accepted + +## Context + +In order to persist and retrieve data to and from disk without reinventing the +wheel, we will need to use a tool that abstracts persistence functionality. + +## Decision + +We will use SQLAlchemy as the Object Relational Mapper (ORM) for this project. + +## Consequences + +- **Architecture**: Use of SQLAlchemy will be limited to the `infrastructure` + directory. From cc76681223541a3904cc059530e356985604ede1 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 16:05:44 -0500 Subject: [PATCH 20/25] Use Iterable type instead of List in repository interface --- nad_ch/domain/repositories.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nad_ch/domain/repositories.py b/nad_ch/domain/repositories.py index d9ace1a..cf9c1fa 100644 --- a/nad_ch/domain/repositories.py +++ b/nad_ch/domain/repositories.py @@ -1,4 +1,5 @@ -from typing import List, Protocol +from typing import Protocol +from collections.abc import Iterable from nad_ch.domain.entities import DataProvider @@ -9,5 +10,5 @@ def add(self, provider: DataProvider) -> None: def get_by_name(self, name: str) -> DataProvider: ... - def get_all(self) -> List[DataProvider]: + def get_all(self) -> Iterable[DataProvider]: ... From 1cac6394819170799bc95044c13d64d9aa3fb280 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 16:23:11 -0500 Subject: [PATCH 21/25] Add id attribute to entities, update mock repository to create ids, update tests --- nad_ch/domain/entities.py | 6 ++++-- nad_ch/infrastructure/database.py | 4 ++-- tests/infrastructure/test_database.py | 1 + tests/mocks.py | 5 ++++- tests/test_use_cases.py | 15 ++++++++++++++- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/nad_ch/domain/entities.py b/nad_ch/domain/entities.py index 6d56a45..1f3d65e 100644 --- a/nad_ch/domain/entities.py +++ b/nad_ch/domain/entities.py @@ -1,9 +1,11 @@ class DataProvider: - def __init__(self, name: str): + def __init__(self, name: str, id: int = None): + self.id = id self.name = name class DataSubmission: - def __init__(self, file_path: str, provider: DataProvider): + def __init__(self, file_path: str, provider: DataProvider, id: int = None): + self.id = id self.file_path = file_path self.provider = provider diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 6a1a130..eae7967 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -35,10 +35,10 @@ class DataProviderModel(ModelBase): @staticmethod def from_entity(provider): - return DataProviderModel(name=provider.name) + return DataProviderModel(id=provider.id, name=provider.name) def to_entity(self): - return DataProvider(name=self.name) + return DataProvider(id=self.id, name=self.name) class SqlAlchemyDataProviderRepository(DataProviderRepository): diff --git a/tests/infrastructure/test_database.py b/tests/infrastructure/test_database.py index f487291..953167f 100644 --- a/tests/infrastructure/test_database.py +++ b/tests/infrastructure/test_database.py @@ -40,5 +40,6 @@ def test_add_data_provider_to_repository_and_get_by_name(providers): providers.add(new_provider) retreived_provider = providers.get_by_name(provider_name) + assert retreived_provider.id == 1 assert retreived_provider.name == provider_name assert isinstance(retreived_provider, DataProvider) is True diff --git a/tests/mocks.py b/tests/mocks.py index dc24205..f7b25f2 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -5,12 +5,15 @@ class MockDataProviderRepository(DataProviderRepository): def __init__(self) -> None: self._providers = set() + self._next_id = 1 def add(self, provider: DataProvider) -> None: + provider.id = self._next_id self._providers.add(provider) + self._next_id += 1 def get_by_name(self, name: str) -> DataProvider: return next(p for p in self._providers if p.name == name) def get_all(self): - return list(self._providers) + return sorted(list(self._providers), key=lambda provider: provider.id) diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index 3e87c5c..a05dc2c 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -28,7 +28,7 @@ def test_add_data_provider_throws_error_if_no_provider_name_given(app_context): add_data_provider(app_context, '') -def test_list_data_providers(app_context): +def test_list_a_single_data_provider(app_context): name = 'State X' add_data_provider(app_context, name) @@ -36,3 +36,16 @@ def test_list_data_providers(app_context): assert len(providers) == 1 assert providers[0].name == name + + +def test_list_multiple_data_providers(app_context): + first_name = 'State X' + add_data_provider(app_context, first_name) + + second_name = 'State Y' + add_data_provider(app_context, second_name) + + providers = list_data_providers(app_context) + assert len(providers) == 2 + assert providers[0].name == first_name + assert providers[1].name == second_name From cbd6004a6f3ff0acb5c1ef35aeaf8a04c56d7696 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 13 Dec 2023 19:36:15 -0500 Subject: [PATCH 22/25] Handle logging via app context and move exception handling to use case --- nad_ch/application_context.py | 11 +++++++++++ nad_ch/config.py | 1 + nad_ch/controllers/cli.py | 15 ++------------- nad_ch/infrastructure/logger.py | 26 ++++++++++++++++++++++++++ nad_ch/use_cases.py | 26 +++++++++++++++++--------- poetry.lock | 12 +++++++++++- pyproject.toml | 1 + 7 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 nad_ch/infrastructure/logger.py diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 68197ad..48bdc5f 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -3,26 +3,37 @@ session_scope, SqlAlchemyDataProviderRepository ) +from nad_ch.infrastructure.logger import Logger from tests.mocks import MockDataProviderRepository class ApplicationContext: def __init__(self): self._providers = SqlAlchemyDataProviderRepository(session_scope) + self._logger = Logger(__name__) @property def providers(self): return self._providers + @property + def logger(self): + return self._logger + class TestApplicationContext(ApplicationContext): def __init__(self): self._providers = MockDataProviderRepository() + self._logger = Logger(__name__) @property def providers(self): return self._providers + @property + def logger(self): + return self._logger + def create_app_context(): if os.environ.get('APP_ENV') == 'test': diff --git a/nad_ch/config.py b/nad_ch/config.py index 3053bd9..c61f8f5 100644 --- a/nad_ch/config.py +++ b/nad_ch/config.py @@ -5,4 +5,5 @@ load_dotenv() +APP_ENV = os.getenv('APP_ENV') DATABASE_URL = os.getenv('DATABASE_URL') diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index 9152e01..00398de 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -3,7 +3,6 @@ add_data_provider, list_data_providers, ingest_data_submission, - InvalidProviderNameException ) @@ -18,23 +17,14 @@ def cli(ctx): @click.argument('provider_name') def add_provider(ctx, provider_name): context = ctx.obj - try: - add_data_provider(context, provider_name) - except InvalidProviderNameException as e: - click.echo(f"Error: {e.message}") - return - - click.echo('Provider added') + add_data_provider(context, provider_name) @cli.command() @click.pass_context def list_providers(ctx): context = ctx.obj - providers = list_data_providers(context) - click.echo('Data Provider Names:') - for p in providers: - click.echo(p.name) + list_data_providers(context) @cli.command() @@ -44,4 +34,3 @@ def list_providers(ctx): def ingest(ctx, file_path, provider): context = ctx.obj ingest_data_submission(context, file_path, provider) - click.echo('Ingest complete') diff --git a/nad_ch/infrastructure/logger.py b/nad_ch/infrastructure/logger.py new file mode 100644 index 0000000..16be36d --- /dev/null +++ b/nad_ch/infrastructure/logger.py @@ -0,0 +1,26 @@ +import logging +from nad_ch.config import APP_ENV + + +class Logger: + def __init__(self, name=__name__): + self.logger = logging.getLogger(name) + if APP_ENV == 'test': + self.logger.setLevel(logging.DEBUG) + else: + self.logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + def info(self, message): + self.logger.info(message) + + def error(self, message): + self.logger.error(message) + + def warning(self, message): + self.logger.warning(message) diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 560a13a..bd82c43 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -5,18 +5,26 @@ def add_data_provider( ctx: ApplicationContext, provider_name: str ) -> None: - if not provider_name: - raise InvalidProviderNameException() + try: + if not provider_name: + raise InvalidProviderNameException() - provider = DataProvider(provider_name) - ctx.providers.add(provider) + provider = DataProvider(provider_name) + ctx.providers.add(provider) + ctx.logger.info('Provider added') + except InvalidProviderNameException as e: + ctx.logger.error(f'Failed to add data provider: {e}') + raise -def list_data_providers( - ctx: ApplicationContext -): - list = ctx.providers.get_all() - return list + +def list_data_providers(ctx: ApplicationContext): + providers = ctx.providers.get_all() + ctx.logger.info('Data Provider Names:') + for p in providers: + ctx.logger.info(p.name) + + return providers def ingest_data_submission( diff --git a/poetry.lock b/poetry.lock index 775bf79..368e678 100644 --- a/poetry.lock +++ b/poetry.lock @@ -187,6 +187,16 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "logging" +version = "0.4.9.6" +description = "A logging module for Python" +optional = false +python-versions = "*" +files = [ + {file = "logging-0.4.9.6.tar.gz", hash = "sha256:26f6b50773f085042d301085bd1bf5d9f3735704db9f37c1ce6d8b85c38f2417"}, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -398,4 +408,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "25f51c569e7fc868fa1b8d90964aee5ee4a27f52b164fd1a614f92bdfa699a96" +content-hash = "5454dd80f8dc2fbbd6d628df561a6a018ef5cd5e44cbc3b979fe56f21ee46353" diff --git a/pyproject.toml b/pyproject.toml index 6d6becb..9da1888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ python = "^3.11" click = "^8.1.7" sqlalchemy = "^2.0.23" pytest-env = "^1.1.3" +logging = "^0.4.9.6" [tool.poetry.group.dev.dependencies] pytest = "^7.4.2" From b40cd62722e33234fc9991b4ca01db6d13047b1b Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Thu, 14 Dec 2023 14:48:00 -0500 Subject: [PATCH 23/25] Change the way use case handles invalid input --- nad_ch/use_cases.py | 22 ++++++---------------- poetry.lock | 19 ++++++++++++++++++- pyproject.toml | 1 + tests/test_use_cases.py | 8 ++++---- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index bd82c43..4198b56 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -5,17 +5,13 @@ def add_data_provider( ctx: ApplicationContext, provider_name: str ) -> None: - try: - if not provider_name: - raise InvalidProviderNameException() + if not provider_name: + ctx.logger.error('Provider name required') + return - provider = DataProvider(provider_name) - ctx.providers.add(provider) - ctx.logger.info('Provider added') - - except InvalidProviderNameException as e: - ctx.logger.error(f'Failed to add data provider: {e}') - raise + provider = DataProvider(provider_name) + ctx.providers.add(provider) + ctx.logger.info('Provider added') def list_data_providers(ctx: ApplicationContext): @@ -31,9 +27,3 @@ def ingest_data_submission( ctx: ApplicationContext, file_path: str, provider_name: str ) -> None: pass - - -class InvalidProviderNameException(Exception): - def __init__(self, message='Provider name is required'): - self.message = message - super().__init__(self.message) diff --git a/poetry.lock b/poetry.lock index 368e678..d4b1dd0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -293,6 +293,23 @@ pytest = ">=7.4.3" [package.extras] test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] +[[package]] +name = "pytest-mock" +version = "3.12.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dotenv" version = "1.0.0" @@ -408,4 +425,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "5454dd80f8dc2fbbd6d628df561a6a018ef5cd5e44cbc3b979fe56f21ee46353" +content-hash = "23da1118dfa9ab08f23e4120e2e5f812bbb8e801f650ac1b2c79a96b033d7153" diff --git a/pyproject.toml b/pyproject.toml index 9da1888..b257fd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ pytest = "^7.4.2" flake8 = "^6.1.0" coverage = "^7.3.2" python-dotenv = "^1.0.0" +pytest-mock = "^3.12.0" [tool.poetry.scripts] init-db="scripts.init_db:main" diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index a05dc2c..00d3e6b 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -4,7 +4,6 @@ from nad_ch.use_cases import ( add_data_provider, list_data_providers, - InvalidProviderNameException ) @@ -23,9 +22,10 @@ def test_add_data_provider(app_context): assert isinstance(provider, DataProvider) is True -def test_add_data_provider_throws_error_if_no_provider_name_given(app_context): - with pytest.raises(InvalidProviderNameException): - add_data_provider(app_context, '') +def test_add_data_provider_throws_error_if_no_provider_name_given(mocker): + mock_context = mocker.patch('nad_ch.application_context.create_app_context') + add_data_provider(mock_context, '') + mock_context.logger.error.assert_called_once_with('Provider name required') def test_list_a_single_data_provider(app_context): From f2df7c8979c58510814790735771978e0372055b Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Thu, 14 Dec 2023 15:18:59 -0500 Subject: [PATCH 24/25] Add validation to ensure provider name is unique --- nad_ch/infrastructure/database.py | 6 +++--- nad_ch/use_cases.py | 8 +++++++- tests/mocks.py | 5 +++-- tests/test_use_cases.py | 10 +++++++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index eae7967..2f34234 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.orm import sessionmaker, declarative_base import contextlib @@ -51,14 +51,14 @@ def add(self, provider: DataProvider): session.add(provider_model) return provider_model.to_entity() - def get_by_name(self, name: str) -> DataProvider: + def get_by_name(self, name: str) -> Optional[DataProvider]: with self.session_factory() as session: provider_model = ( session.query(DataProviderModel) .filter(DataProviderModel.name == name) .first() ) - return provider_model.to_entity() + return provider_model.to_entity() if provider_model else None def get_all(self) -> List[DataProvider]: with self.session_factory() as session: diff --git a/nad_ch/use_cases.py b/nad_ch/use_cases.py index 4198b56..cbf764b 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,3 +1,4 @@ +from typing import List from nad_ch.application_context import ApplicationContext from nad_ch.domain.entities import DataProvider @@ -9,12 +10,17 @@ def add_data_provider( ctx.logger.error('Provider name required') return + matching_provider = ctx.providers.get_by_name(provider_name) + if matching_provider: + ctx.logger.error('Provider name must be unique') + return + provider = DataProvider(provider_name) ctx.providers.add(provider) ctx.logger.info('Provider added') -def list_data_providers(ctx: ApplicationContext): +def list_data_providers(ctx: ApplicationContext) -> List[DataProvider]: providers = ctx.providers.get_all() ctx.logger.info('Data Provider Names:') for p in providers: diff --git a/tests/mocks.py b/tests/mocks.py index f7b25f2..1b80e2b 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,3 +1,4 @@ +from typing import Optional from nad_ch.domain.entities import DataProvider from nad_ch.domain.repositories import DataProviderRepository @@ -12,8 +13,8 @@ def add(self, provider: DataProvider) -> None: self._providers.add(provider) self._next_id += 1 - def get_by_name(self, name: str) -> DataProvider: - return next(p for p in self._providers if p.name == name) + def get_by_name(self, name: str) -> Optional[DataProvider]: + return next((p for p in self._providers if p.name == name), None) def get_all(self): return sorted(list(self._providers), key=lambda provider: provider.id) diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index 00d3e6b..3cdc84d 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -22,12 +22,20 @@ def test_add_data_provider(app_context): assert isinstance(provider, DataProvider) is True -def test_add_data_provider_throws_error_if_no_provider_name_given(mocker): +def test_add_data_provider_logs_error_if_no_provider_name_given(mocker): mock_context = mocker.patch('nad_ch.application_context.create_app_context') add_data_provider(mock_context, '') mock_context.logger.error.assert_called_once_with('Provider name required') +def test_add_data_provider_logs_error_provider_name_not_unique(mocker): + mock_context = mocker.patch('nad_ch.application_context.create_app_context') + mock_context.providers.get_by_name.return_value('State X') + add_data_provider(mock_context, 'State X') + + mock_context.logger.error.assert_called_once_with('Provider name must be unique') + + def test_list_a_single_data_provider(app_context): name = 'State X' add_data_provider(app_context, name) From 4c76a454853e4494cacad038a1b9efa9a011fc3d Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Thu, 14 Dec 2023 15:22:58 -0500 Subject: [PATCH 25/25] Pass logging level to Logger in constructor, remove env check in Logger --- nad_ch/application_context.py | 3 ++- nad_ch/infrastructure/logger.py | 8 ++------ tests/test_use_cases.py | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 48bdc5f..ede8691 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -1,4 +1,5 @@ import os +import logging from nad_ch.infrastructure.database import ( session_scope, SqlAlchemyDataProviderRepository @@ -24,7 +25,7 @@ def logger(self): class TestApplicationContext(ApplicationContext): def __init__(self): self._providers = MockDataProviderRepository() - self._logger = Logger(__name__) + self._logger = Logger(__name__, logging.DEBUG) @property def providers(self): diff --git a/nad_ch/infrastructure/logger.py b/nad_ch/infrastructure/logger.py index 16be36d..a7959c9 100644 --- a/nad_ch/infrastructure/logger.py +++ b/nad_ch/infrastructure/logger.py @@ -1,14 +1,10 @@ import logging -from nad_ch.config import APP_ENV class Logger: - def __init__(self, name=__name__): + def __init__(self, name=__name__, logger_level=logging.INFO): self.logger = logging.getLogger(name) - if APP_ENV == 'test': - self.logger.setLevel(logging.DEBUG) - else: - self.logger.setLevel(logging.INFO) + self.logger.setLevel(logger_level) handler = logging.StreamHandler() formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' diff --git a/tests/test_use_cases.py b/tests/test_use_cases.py index 3cdc84d..86589da 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -28,7 +28,7 @@ def test_add_data_provider_logs_error_if_no_provider_name_given(mocker): mock_context.logger.error.assert_called_once_with('Provider name required') -def test_add_data_provider_logs_error_provider_name_not_unique(mocker): +def test_add_data_provider_logs_error_if_provider_name_not_unique(mocker): mock_context = mocker.patch('nad_ch.application_context.create_app_context') mock_context.providers.get_by_name.return_value('State X') add_data_provider(mock_context, 'State X')