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/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. diff --git a/nad_ch/application_context.py b/nad_ch/application_context.py index 4b4da37..ede8691 100644 --- a/nad_ch/application_context.py +++ b/nad_ch/application_context.py @@ -1,19 +1,39 @@ import os -from .gateways.storage_mock import StorageGatewayMock +import logging +from nad_ch.infrastructure.database import ( + session_scope, + SqlAlchemyDataProviderRepository +) +from nad_ch.infrastructure.logger import Logger +from tests.mocks import MockDataProviderRepository class ApplicationContext: def __init__(self): - self._storage = StorageGatewayMock() + self._providers = SqlAlchemyDataProviderRepository(session_scope) + self._logger = Logger(__name__) @property - def storage(self): - return self._storage + def providers(self): + return self._providers + + @property + def logger(self): + return self._logger class TestApplicationContext(ApplicationContext): def __init__(self): - self._storage = StorageGatewayMock() + self._providers = MockDataProviderRepository() + self._logger = Logger(__name__, logging.DEBUG) + + @property + def providers(self): + return self._providers + + @property + def logger(self): + return self._logger def create_app_context(): diff --git a/nad_ch/config.py b/nad_ch/config.py new file mode 100644 index 0000000..c61f8f5 --- /dev/null +++ b/nad_ch/config.py @@ -0,0 +1,9 @@ +from dotenv import load_dotenv +import os + + +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 5d4e841..00398de 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -1,6 +1,9 @@ import click -from ..entities import File -from ..use_cases import upload_file, list_files, get_file_metadata +from nad_ch.use_cases import ( + add_data_provider, + list_data_providers, + ingest_data_submission, +) @click.group() @@ -11,28 +14,23 @@ def cli(ctx): @cli.command() @click.pass_context -@click.argument('filename') -@click.argument('content') -def upload(ctx, filename, content): +@click.argument('provider_name') +def add_provider(ctx, provider_name): context = ctx.obj - file = File(name=filename, content=content) - upload_file(context, file) - click.echo(f"Uploaded {filename}") + add_data_provider(context, provider_name) @cli.command() @click.pass_context -def listall(ctx): +def list_providers(ctx): context = ctx.obj - files = list_files(context) - for file in files: - click.echo(file.name) + list_data_providers(context) @cli.command() @click.pass_context -@click.argument('filename') -def metadata(ctx, filename): +@click.argument('filepath') +@click.argument('provider') +def ingest(ctx, file_path, provider): 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, provider) diff --git a/nad_ch/domain/entities.py b/nad_ch/domain/entities.py new file mode 100644 index 0000000..1f3d65e --- /dev/null +++ b/nad_ch/domain/entities.py @@ -0,0 +1,11 @@ +class DataProvider: + def __init__(self, name: str, id: int = None): + self.id = id + self.name = name + + +class DataSubmission: + 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/domain/repositories.py b/nad_ch/domain/repositories.py new file mode 100644 index 0000000..cf9c1fa --- /dev/null +++ b/nad_ch/domain/repositories.py @@ -0,0 +1,14 @@ +from typing import Protocol +from collections.abc import Iterable +from nad_ch.domain.entities import DataProvider + + +class DataProviderRepository(Protocol): + def add(self, provider: DataProvider) -> None: + ... + + def get_by_name(self, name: str) -> DataProvider: + ... + + def get_all(self) -> Iterable[DataProvider]: + ... diff --git a/nad_ch/entities.py b/nad_ch/entities.py deleted file mode 100644 index db3a703..0000000 --- a/nad_ch/entities.py +++ /dev/null @@ -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/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..2f34234 --- /dev/null +++ b/nad_ch/infrastructure/database.py @@ -0,0 +1,67 @@ +from typing import List, Optional +from sqlalchemy import Column, Integer, String, create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +import contextlib +from nad_ch.config import DATABASE_URL +from nad_ch.domain.entities import DataProvider +from nad_ch.domain.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() + + +class DataProviderModel(ModelBase): + __tablename__ = 'data_providers' + + id = Column(Integer, primary_key=True) + name = Column(String) + + @staticmethod + def from_entity(provider): + return DataProviderModel(id=provider.id, name=provider.name) + + def to_entity(self): + return DataProvider(id=self.id, name=self.name) + + +class SqlAlchemyDataProviderRepository(DataProviderRepository): + def __init__(self, session_factory): + self.session_factory = session_factory + + def add(self, provider: DataProvider): + with self.session_factory() as session: + provider_model = DataProviderModel.from_entity(provider) + session.add(provider_model) + return provider_model.to_entity() + + 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() if provider_model else None + + 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] + return providers_entities diff --git a/nad_ch/infrastructure/logger.py b/nad_ch/infrastructure/logger.py new file mode 100644 index 0000000..a7959c9 --- /dev/null +++ b/nad_ch/infrastructure/logger.py @@ -0,0 +1,22 @@ +import logging + + +class Logger: + def __init__(self, name=__name__, logger_level=logging.INFO): + self.logger = logging.getLogger(name) + self.logger.setLevel(logger_level) + 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/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/main.py b/nad_ch/main.py index 01d8463..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/use_cases.py b/nad_ch/use_cases.py index 16c6229..cbf764b 100644 --- a/nad_ch/use_cases.py +++ b/nad_ch/use_cases.py @@ -1,18 +1,35 @@ -from .application_context import ApplicationContext -from .entities import File, FileMetadata -from .interfaces.storage import StorageGateway +from typing import List +from nad_ch.application_context import ApplicationContext +from nad_ch.domain.entities import DataProvider -def upload_file(ctx: ApplicationContext, file: File) -> None: - storage: StorageGateway = ctx.storage - storage.save(file) +def add_data_provider( + ctx: ApplicationContext, provider_name: str +) -> None: + if not provider_name: + 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 -def list_files(ctx: ApplicationContext) -> list[File]: - storage: StorageGateway = ctx.storage - return storage.list_all() + provider = DataProvider(provider_name) + ctx.providers.add(provider) + ctx.logger.info('Provider added') -def get_file_metadata(ctx: ApplicationContext, file_name: str) -> FileMetadata: - storage: StorageGateway = ctx.storage - return storage.get_metadata(file_name) +def list_data_providers(ctx: ApplicationContext) -> List[DataProvider]: + 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( + ctx: ApplicationContext, file_path: str, provider_name: str +) -> None: + pass diff --git a/poetry.lock b/poetry.lock index b79eecf..d4b1dd0 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" @@ -116,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" @@ -177,13 +258,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] @@ -195,7 +276,153 @@ 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 = "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" +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" +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 = "23da1118dfa9ab08f23e4120e2e5f812bbb8e801f650ac1b2c79a96b033d7153" diff --git a/pyproject.toml b/pyproject.toml index 2758f62..b257fd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,12 +11,25 @@ authors = [] [tool.poetry.dependencies] 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" 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" +lint = "flake8.main.cli:main" start = "nad_ch.main:main" test = "pytest:main" + +[tool.pytest.ini_options] +env = [ + "APP_ENV=test", + "DATABASE_URL=sqlite:///:memory:" +] diff --git a/scripts/init_db.py b/scripts/init_db.py new file mode 100644 index 0000000..3573c7f --- /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 +from nad_ch.config import DATABASE_URL + + +def main(): + 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__': + main() 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..953167f --- /dev/null +++ b/tests/infrastructure/test_database.py @@ -0,0 +1,45 @@ +import pytest +import contextlib +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from nad_ch.config import DATABASE_URL +from nad_ch.domain.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.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 new file mode 100644 index 0000000..1b80e2b --- /dev/null +++ b/tests/mocks.py @@ -0,0 +1,20 @@ +from typing import Optional +from nad_ch.domain.entities import DataProvider +from nad_ch.domain.repositories import DataProviderRepository + + +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) -> 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 acf57a5..86589da 100644 --- a/tests/test_use_cases.py +++ b/tests/test_use_cases.py @@ -1,42 +1,59 @@ 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.domain.entities import DataProvider +from nad_ch.use_cases import ( + add_data_provider, + list_data_providers, +) -@pytest.fixture(scope="function") +@pytest.fixture(scope='function') def app_context(): context = create_app_context() yield context -def test_upload_file(app_context): - file = File(name="test.txt", content="Sample content") +def test_add_data_provider(app_context): + name = 'State X' + add_data_provider(app_context, name) - upload_file(app_context, file) + provider = app_context.providers.get_by_name(name) + assert provider.name == name + assert isinstance(provider, DataProvider) is True - stored_file = app_context.storage.get_file("test.txt") - assert stored_file.name == "test.txt" - assert stored_file.content == "Sample content" +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_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) +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') - assert len(files) == 2 - assert any(f.name == "test1.txt" for f in files) - assert any(f.name == "test2.txt" for f in files) + mock_context.logger.error.assert_called_once_with('Provider name must be unique') -def test_get_file_metadata(app_context): - file = File(name="test.txt", content="Sample content for metadata") - upload_file(app_context, file) +def test_list_a_single_data_provider(app_context): + name = 'State X' + add_data_provider(app_context, name) - metadata = get_file_metadata(app_context, "test.txt") + providers = list_data_providers(app_context) - assert isinstance(metadata, FileMetadata) + 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