From 99b78006a0eefc982ddc8b527265bac87ef19673 Mon Sep 17 00:00:00 2001 From: Pouria Khalaj Date: Wed, 15 May 2024 13:39:57 +0200 Subject: [PATCH] Make ruff happy. --- trolldb/api/routes/databases.py | 4 ++++ trolldb/api/routes/datetime_.py | 5 +++++ trolldb/api/routes/platforms.py | 1 + trolldb/api/routes/queries.py | 9 +++++---- trolldb/api/routes/root.py | 1 + trolldb/api/routes/sensors.py | 1 + trolldb/api/tests/conftest.py | 10 +++++++--- trolldb/api/tests/test_api.py | 14 ++++++++++--- trolldb/database/errors.py | 14 ++++++------- trolldb/database/piplines.py | 15 ++++++++++++++ trolldb/errors/errors.py | 26 +++++++++++++++++-------- trolldb/run_api.py | 2 +- trolldb/test_utils/common.py | 15 ++++++++------ trolldb/test_utils/mongodb_database.py | 27 ++++++++++++++++++++++---- trolldb/test_utils/mongodb_instance.py | 13 ++++++++++--- 15 files changed, 117 insertions(+), 40 deletions(-) diff --git a/trolldb/api/routes/databases.py b/trolldb/api/routes/databases.py index 798e798..fa92264 100644 --- a/trolldb/api/routes/databases.py +++ b/trolldb/api/routes/databases.py @@ -24,6 +24,7 @@ response_model=list[str], summary="Gets the list of all database names") async def database_names(exclude_defaults: bool = exclude_defaults_query) -> list[str]: + """TODO.""" db_names = await MongoDB.list_database_names() if not exclude_defaults: @@ -37,6 +38,7 @@ async def database_names(exclude_defaults: bool = exclude_defaults_query) -> lis responses=Databases.union().fastapi_descriptor, summary="Gets the list of all collection names for the given database name") async def collection_names(db: CheckDataBaseDependency) -> list[str]: + """TODO.""" return await db.list_collection_names() @@ -45,6 +47,7 @@ async def collection_names(db: CheckDataBaseDependency) -> list[str]: responses=database_collection_error_descriptor, summary="Gets the object ids of all documents for the given database and collection name") async def documents(collection: CheckCollectionDependency) -> list[str]: + """TODO.""" return await get_ids(collection.find({})) @@ -53,6 +56,7 @@ async def documents(collection: CheckCollectionDependency) -> list[str]: responses=database_collection_document_error_descriptor, summary="Gets the document content in json format given its object id, database, and collection name") async def document_by_id(collection: CheckCollectionDependency, _id: MongoObjectId) -> _DocumentType: + """TODO.""" if document := await collection.find_one({"_id": _id}): return dict(document) | {"_id": str(_id)} diff --git a/trolldb/api/routes/datetime_.py b/trolldb/api/routes/datetime_.py index f5d4e48..78d412d 100644 --- a/trolldb/api/routes/datetime_.py +++ b/trolldb/api/routes/datetime_.py @@ -16,16 +16,19 @@ class TimeModel(TypedDict): + """TODO.""" _id: str _time: datetime class TimeEntry(TypedDict): + """TODO.""" _min: TimeModel _max: TimeModel class ResponseModel(BaseModel): + """TODO.""" start_time: TimeEntry end_time: TimeEntry @@ -38,6 +41,7 @@ class ResponseModel(BaseModel): responses=database_collection_error_descriptor, summary="Gets the the minimum and maximum values for the start and end times") async def datetime(collection: CheckCollectionDependency) -> ResponseModel: + """TODO.""" agg_result = await collection.aggregate([{ "$group": { "_id": None, @@ -48,6 +52,7 @@ async def datetime(collection: CheckCollectionDependency) -> ResponseModel: }}]).next() def _aux(query): + """TODO.""" return get_id(collection.find_one(query)) return ResponseModel( diff --git a/trolldb/api/routes/platforms.py b/trolldb/api/routes/platforms.py index 4e6e868..53708a7 100644 --- a/trolldb/api/routes/platforms.py +++ b/trolldb/api/routes/platforms.py @@ -17,4 +17,5 @@ responses=database_collection_error_descriptor, summary="Gets the list of all platform names") async def platform_names(collection: CheckCollectionDependency) -> list[str]: + """TODO.""" return await get_distinct_items_in_collection(collection, "platform_name") diff --git a/trolldb/api/routes/queries.py b/trolldb/api/routes/queries.py index 20d3984..78284dd 100644 --- a/trolldb/api/routes/queries.py +++ b/trolldb/api/routes/queries.py @@ -22,10 +22,11 @@ summary="Gets the database UUIDs of the documents that match specifications determined by the query string") async def queries( collection: CheckCollectionDependency, - platform: list[str] = Query(None), - sensor: list[str] = Query(None), - time_min: datetime.datetime = Query(None), - time_max: datetime.datetime = Query(None)) -> list[str]: + platform: list[str] = Query(default=None), # noqa: B008 + sensor: list[str] = Query(default=None), # noqa: B008 + time_min: datetime.datetime = Query(default=None), # noqa: B008 + time_max: datetime.datetime = Query(default=None)) -> list[str]: # noqa: B008 + """TODO.""" pipelines = Pipelines() if platform: diff --git a/trolldb/api/routes/root.py b/trolldb/api/routes/root.py index 5099fe9..3fa975f 100644 --- a/trolldb/api/routes/root.py +++ b/trolldb/api/routes/root.py @@ -11,4 +11,5 @@ @router.get("/", summary="The root route which is mainly used to check the status of connection") async def root() -> Response: + """TODO.""" return Response(status_code=status.HTTP_200_OK) diff --git a/trolldb/api/routes/sensors.py b/trolldb/api/routes/sensors.py index 2fa5d0b..826b163 100644 --- a/trolldb/api/routes/sensors.py +++ b/trolldb/api/routes/sensors.py @@ -17,4 +17,5 @@ responses=database_collection_error_descriptor, summary="Gets the list of all sensor names") async def sensor_names(collection: CheckCollectionDependency) -> list[str]: + """TODO.""" return await get_distinct_items_in_collection(collection, "sensor") diff --git a/trolldb/api/tests/conftest.py b/trolldb/api/tests/conftest.py index 5a8d7c1..b9a3a6d 100644 --- a/trolldb/api/tests/conftest.py +++ b/trolldb/api/tests/conftest.py @@ -1,3 +1,5 @@ +"""TODO.""" + import pytest from trolldb.api.api import server_process_context @@ -7,13 +9,15 @@ @pytest.fixture(scope="session") -def run_mongodb_server_instance(): +def _run_mongodb_server_instance(): + """TODO.""" with mongodb_instance_server_process_context(): yield -@pytest.fixture(scope="session", autouse=True) -def test_server_fixture(run_mongodb_server_instance): +@pytest.fixture(scope="session") +def _test_server_fixture(_run_mongodb_server_instance): + """TODO.""" TestDatabase.prepare() with server_process_context(test_app_config, startup_time=2000): yield diff --git a/trolldb/api/tests/test_api.py b/trolldb/api/tests/test_api.py index 445b56a..97743ee 100644 --- a/trolldb/api/tests/test_api.py +++ b/trolldb/api/tests/test_api.py @@ -1,25 +1,30 @@ +"""TODO.""" +import pytest from fastapi import status from trolldb.test_utils.common import assert_equal, http_get from trolldb.test_utils.mongodb_database import TestDatabase, test_mongodb_context +@pytest.mark.usefixtures("_test_server_fixture") def test_root(): """Checks that the server is up and running, i.e. the root routes responds with 200.""" assert_equal(http_get().status, status.HTTP_200_OK) +@pytest.mark.usefixtures("_test_server_fixture") def test_platforms(): """Checks that the retrieved platform names match the expected names.""" assert_equal(http_get("platforms").json(), TestDatabase.platform_names) +@pytest.mark.usefixtures("_test_server_fixture") def test_sensors(): - """Checks that the retrieved sensor names match the expected names. - """ + """Checks that the retrieved sensor names match the expected names.""" assert_equal(http_get("sensors").json(), TestDatabase.sensors) +@pytest.mark.usefixtures("_test_server_fixture") def test_database_names(): """Checks that the retrieved database names match the expected names.""" assert_equal(http_get("databases").json(), TestDatabase.database_names) @@ -27,11 +32,13 @@ def test_database_names(): assert_equal(http_get("databases?exclude_defaults=False").json(), TestDatabase.all_database_names) +@pytest.mark.usefixtures("_test_server_fixture") def test_database_names_negative(): """Checks that the non-existing databases cannot be found.""" assert_equal(http_get("databases/non_existing_database").status, status.HTTP_404_NOT_FOUND) +@pytest.mark.usefixtures("_test_server_fixture") def test_collections(): """Check the presence of existing collections and that the ids of documents therein can be correctly retrieved.""" with test_mongodb_context() as client: @@ -50,9 +57,10 @@ def test_collections(): ) +@pytest.mark.usefixtures("_test_server_fixture") def test_collections_negative(): """Checks that the non-existing collections cannot be found.""" - for database_name, collection_name in zip(TestDatabase.database_names, TestDatabase.collection_names, strict=False): + for database_name in TestDatabase.database_names: assert_equal( http_get(f"databases/{database_name}/non_existing_collection").status, status.HTTP_404_NOT_FOUND diff --git a/trolldb/database/errors.py b/trolldb/database/errors.py index a15ec43..044181f 100644 --- a/trolldb/database/errors.py +++ b/trolldb/database/errors.py @@ -1,5 +1,4 @@ -"""The modules which defines the error responses that might occur while working with the -MongoDB database. +"""The modules which defines the error responses that might occur while working with the MongoDB database. Note: The error responses are grouped into classes, with each class representing the major @@ -79,14 +78,13 @@ class Documents(ResponsesErrorGroup): database_collection_error_descriptor = ( Databases.union() | Collections.union() ).fastapi_descriptor -""" -A response descriptor for the Fast API routes. This combines all the error messages that might -occur as result of working with databases and collections. See the fast api documentation for TODO. +"""A response descriptor for the Fast API routes. + +This combines all the error messages that might occur as result of working with databases and collections. See the +fast api documentation for TODO. """ database_collection_document_error_descriptor = ( Databases.union() | Collections.union() | Documents.union() ).fastapi_descriptor -""" -Same as :obj:`database_collection_error_descriptor` but including documents as well. -""" +"""Same as :obj:`database_collection_error_descriptor` but including documents as well.""" diff --git a/trolldb/database/piplines.py b/trolldb/database/piplines.py index 8fb95d5..eeb0cac 100644 --- a/trolldb/database/piplines.py +++ b/trolldb/database/piplines.py @@ -21,45 +21,60 @@ class PipelineDict(dict): """ def __or__(self, other: Self): + """TODO.""" return PipelineDict({"$or": [self, other]}) def __and__(self, other: Self): + """TODO.""" return PipelineDict({"$and": [self, other]}) class PipelineAttribute: + """TODO.""" + def __init__(self, key: str): + """TODO.""" self.__key = key def __eq__(self, other: Any) -> PipelineDict: + """TODO.""" if isinstance(other, list): return PipelineDict(**{"$or": [{self.__key: v} for v in other]}) return PipelineDict(**{self.__key: other}) def __aux_operators(self, other: Any, operator: str) -> PipelineDict: + """TODO.""" return PipelineDict(**{self.__key: {operator: other}} if other else {}) def __ge__(self, other: Any) -> PipelineDict: + """TODO.""" return self.__aux_operators(other, "$gte") def __gt__(self, other: Any) -> PipelineDict: + """TODO.""" return self.__aux_operators(other, "$gt") def __le__(self, other: Any) -> PipelineDict: + """TODO.""" return self.__aux_operators(other, "$lte") def __lt__(self, other: Any) -> PipelineDict: + """TODO.""" return self.__aux_operators(other, "$le") class Pipelines(list): + """TODO.""" def __init__(self, *args, **kwargs): + """TODO.""" super().__init__(*args, **kwargs) def __iadd__(self, other): + """TODO.""" self.extend([{"$match": other}]) return self def __add__(self, other): + """TODO.""" self.append({"$match": other}) return self diff --git a/trolldb/errors/errors.py b/trolldb/errors/errors.py index c6c1c54..eeb7551 100644 --- a/trolldb/errors/errors.py +++ b/trolldb/errors/errors.py @@ -1,4 +1,5 @@ """The module which defines the base functionality for error responses that will be returned by the API. + This module only includes the generic utilities using which each module should define its own error responses specifically. See :obj:`trolldb.database.errors` as an example on how this module is used. """ @@ -18,19 +19,18 @@ class ResponseError(Exception): """The base class for all error responses. This is derivative of the ``Exception`` class.""" descriptor_delimiter: str = " |OR| " - """ - A delimiter to combine the message part of several error responses into a single one. This will be shown in textual - format for the response descriptors of the Fast API routes. For example: + """A delimiter to combine the message part of several error responses into a single one. + + This will be shown in textual format for the response descriptors of the Fast API routes. For example: ``ErrorA |OR| ErrorB`` """ - defaultResponseClass: Response = PlainTextResponse - """ - The default type of the response which will be returned when an error occurs. - """ + DefaultResponseClass: Response = PlainTextResponse + """The default type of the response which will be returned when an error occurs.""" def __init__(self, args_dict: OrderedDict[StatusCode, str | list[str]] | dict) -> None: + """TODO.""" self.__dict: OrderedDict = OrderedDict(args_dict) self.extra_information: dict | None = None @@ -64,6 +64,7 @@ def __or__(self, other: Self): def __assert_existence_multiple_response_codes( self, status_code: StatusCode | None = None) -> (StatusCode, str): + """TODO.""" match status_code, len(self.__dict): case None, n if n > 1: raise ValueError("In case of multiple response status codes, the status code must be specified.") @@ -80,6 +81,7 @@ def get_error_details( self, extra_information: dict | None = None, status_code: int | None = None) -> (StatusCode, str): + """TODO.""" status_code, msg = self.__assert_existence_multiple_response_codes(status_code) return ( status_code, @@ -91,6 +93,7 @@ def sys_exit_log( exit_code: int = -1, extra_information: dict | None = None, status_code: int | None = None) -> None: + """TODO.""" msg, _ = self.get_error_details(extra_information, status_code) logger.error(msg) exit(exit_code) @@ -99,32 +102,39 @@ def log_as_warning( self, extra_information: dict | None = None, status_code: int | None = None): + """TODO.""" msg, _ = self.get_error_details(extra_information, status_code) logger.warning(msg) @property def fastapi_descriptor(self) -> dict[StatusCode, dict[Literal["description"], str]]: + """TODO.""" return {status: {Literal["description"]: ResponseError.__stringify(msg)} for status, msg in self.__dict.items()} @staticmethod def __listify(item: str | list[str]) -> list[str]: + """TODO.""" return item if isinstance(item, list) else [item] @staticmethod def __stringify(item: str | list[str]) -> str: + """TODO.""" return ResponseError.descriptor_delimiter.join(ResponseError.__listify(item)) class ResponsesErrorGroup: + """TODO.""" @classmethod def fields(cls): + """TODO.""" return {k: v for k, v in cls.__dict__.items() if isinstance(v, ResponseError)} @classmethod def union(cls): + """TODO.""" buff = None - for k, v in cls.fields().items(): + for v in cls.fields().values(): if buff is None: buff = v else: diff --git a/trolldb/run_api.py b/trolldb/run_api.py index d1b0ea2..b5bc6cf 100644 --- a/trolldb/run_api.py +++ b/trolldb/run_api.py @@ -1,4 +1,4 @@ -"""The main entry point to run the API server according to the configurations given in `config.yaml` +"""The main entry point to run the API server according to the configurations given in `config.yaml`. Note: For more information on the API server, see the automatically generated documentation by FastAPI. diff --git a/trolldb/test_utils/common.py b/trolldb/test_utils/common.py index 7994308..f4bb333 100644 --- a/trolldb/test_utils/common.py +++ b/trolldb/test_utils/common.py @@ -1,3 +1,5 @@ +"""TODO.""" + from typing import Any from urllib.parse import urljoin @@ -30,9 +32,10 @@ def http_get(route: str = "") -> BaseHTTPResponse: def assert_equal(test, expected) -> None: - """An auxiliary function to assert the equality of two objects using the ``==`` operator. In case an input is a list or - a tuple, it will be first converted to a set so that the order of items there in does not affect the assertion - outcome. + """An auxiliary function to assert the equality of two objects using the ``==`` operator. + + In case an input is a list or a tuple, it will be first converted to a set so that the order of items there in does + not affect the assertion outcome. Warning: In case of a list or tuple of items as inputs, do not use this function if the order of items matters. @@ -45,8 +48,8 @@ def assert_equal(test, expected) -> None: """ def _setify(obj: Any) -> Any: - """An auxiliary function to convert an object to a set if it is a tuple or a list. - """ + """An auxiliary function to convert an object to a set if it is a tuple or a list.""" return set(obj) if isinstance(obj, list | tuple) else obj - assert _setify(test) == _setify(expected) + if not _setify(test) == _setify(expected): + raise AssertionError(f"{test} and {expected} are not equal.") diff --git a/trolldb/test_utils/mongodb_database.py b/trolldb/test_utils/mongodb_database.py index 9eeb4d2..c79ef18 100644 --- a/trolldb/test_utils/mongodb_database.py +++ b/trolldb/test_utils/mongodb_database.py @@ -1,3 +1,5 @@ +"""TODO.""" + from contextlib import contextmanager from datetime import datetime, timedelta from random import randint, shuffle @@ -11,6 +13,7 @@ @contextmanager def test_mongodb_context(database_config: DatabaseConfig = test_app_config.database): + """TODO.""" client = None try: client = MongoClient(database_config.url.unicode_string(), connectTimeoutMS=database_config.timeout) @@ -21,39 +24,48 @@ def test_mongodb_context(database_config: DatabaseConfig = test_app_config.datab def random_sample(items: list[Any], size=10): + """TODO.""" last_index = len(items) - 1 - indices = [randint(0, last_index) for _ in range(size)] + indices = [randint(0, last_index) for _ in range(size)] # noqa: S311 return [items[i] for i in indices] class Time: + """TODO.""" min_start_time = datetime(2019, 1, 1, 0, 0, 0) max_end_time = datetime(2024, 1, 1, 0, 0, 0) delta_time = int((max_end_time - min_start_time).total_seconds()) @staticmethod def random_interval_secs(max_interval_secs): - return timedelta(seconds=randint(0, max_interval_secs)) + """TODO.""" + return timedelta(seconds=randint(0, max_interval_secs)) # noqa: S311 @staticmethod def random_start_time(): + """TODO.""" return Time.min_start_time + Time.random_interval_secs(Time.delta_time) @staticmethod def random_end_time(start_time: datetime, max_interval_secs: int = 300): + """TODO.""" return start_time + Time.random_interval_secs(max_interval_secs) class Document: + """TODO.""" + def __init__(self, platform_name: str, sensor: str): + """TODO.""" self.platform_name = platform_name self.sensor = sensor self.start_time = Time.random_start_time() self.end_time = Time.random_end_time(self.start_time) def generate_dataset(self, max_count: int): + """TODO.""" dataset = [] - n = randint(1, max_count) + n = randint(1, max_count) # noqa: S311 for i in range(n): txt = f"{self.platform_name}_{self.sensor}_{self.start_time}_{self.end_time}_{i}" dataset.append({ @@ -64,6 +76,7 @@ def generate_dataset(self, max_count: int): return dataset def like_mongodb_document(self): + """TODO.""" return { "platform_name": self.platform_name, "sensor": self.sensor, @@ -74,6 +87,7 @@ def like_mongodb_document(self): class TestDatabase: + """TODO.""" platform_names = random_sample(["PA", "PB", "PC"]) sensors = random_sample(["SA", "SB", "SC"]) @@ -85,13 +99,16 @@ class TestDatabase: @classmethod def generate_documents(cls, random_shuffle=True) -> list: - documents = [Document(p, s).like_mongodb_document() for p, s in zip(cls.platform_names, cls.sensors, strict=False)] + """TODO.""" + documents = [Document(p, s).like_mongodb_document() for p, s in zip(cls.platform_names, cls.sensors, + strict=False)] if random_shuffle: shuffle(documents) return documents @classmethod def reset(cls): + """TODO.""" with test_mongodb_context() as client: for db_name, coll_name in zip(cls.database_names, cls.collection_names, strict=False): db = client[db_name] @@ -101,6 +118,7 @@ def reset(cls): @classmethod def write_mock_date(cls): + """TODO.""" with test_mongodb_context() as client: cls.documents = cls.generate_documents() collection = client[test_app_config.database.main_database_name][ @@ -109,5 +127,6 @@ def write_mock_date(cls): @classmethod def prepare(cls): + """TODO.""" cls.reset() cls.write_mock_date() diff --git a/trolldb/test_utils/mongodb_instance.py b/trolldb/test_utils/mongodb_instance.py index 7102b05..04fe553 100644 --- a/trolldb/test_utils/mongodb_instance.py +++ b/trolldb/test_utils/mongodb_instance.py @@ -1,5 +1,4 @@ -"""The module which defines functionalities to run a MongoDB instance which is to be used in the testing environment. -""" +"""The module which defines functionalities to run a MongoDB instance which is to be used in the testing environment.""" import errno import subprocess import sys @@ -16,6 +15,7 @@ class TestMongoInstance: + """TODO.""" log_dir: str = tempfile.mkdtemp("__pytroll_db_temp_test_log") storage_dir: str = tempfile.mkdtemp("__pytroll_db_temp_test_storage") port: int = 28017 @@ -23,17 +23,20 @@ class TestMongoInstance: @classmethod def prepare_dir(cls, directory: str): + """TODO.""" cls.remove_dir(directory) mkdir(directory) @classmethod def remove_dir(cls, directory: str): + """TODO.""" if path.exists(directory) and path.isdir(directory): rmtree(directory) @classmethod def run_subprocess(cls, args: list[str], wait=True): - cls.process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + """TODO.""" + cls.process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # noqa: S603 if wait: outs, errs = cls.process.communicate() return outs, errs @@ -41,6 +44,7 @@ def run_subprocess(cls, args: list[str], wait=True): @classmethod def mongodb_exists(cls) -> bool: + """TODO.""" outs, errs = cls.run_subprocess(["which", "mongod"]) if outs and not errs: return True @@ -48,17 +52,20 @@ def mongodb_exists(cls) -> bool: @classmethod def prepare_dirs(cls) -> None: + """TODO.""" cls.prepare_dir(cls.log_dir) cls.prepare_dir(cls.storage_dir) @classmethod def run_instance(cls): + """TODO.""" cls.run_subprocess( ["mongod", "--dbpath", cls.storage_dir, "--logpath", f"{cls.log_dir}/mongod.log", "--port", f"{cls.port}"] , wait=False) @classmethod def shutdown_instance(cls): + """TODO.""" cls.process.kill() for d in [cls.log_dir, cls.storage_dir]: cls.remove_dir(d)