diff --git a/docs/sdk/notification.md b/docs/sdk/notification.md index 5e67a0a66..474ce1d14 100644 --- a/docs/sdk/notification.md +++ b/docs/sdk/notification.md @@ -1,4 +1,3 @@ # Notification module -## Queries -::: kili.entrypoints.queries.notification.__init__.QueriesNotification +::: kili.presentation.client.notification.NotificationClientMethods diff --git a/src/kili/adapters/kili_api_gateway/__init__.py b/src/kili/adapters/kili_api_gateway/__init__.py index ccc7a0f6b..857873fc3 100644 --- a/src/kili/adapters/kili_api_gateway/__init__.py +++ b/src/kili/adapters/kili_api_gateway/__init__.py @@ -6,6 +6,9 @@ from kili.adapters.kili_api_gateway.cloud_storage import CloudStorageOperationMixin from kili.adapters.kili_api_gateway.issue import IssueOperationMixin from kili.adapters.kili_api_gateway.label.operations_mixin import LabelOperationMixin +from kili.adapters.kili_api_gateway.notification.operations_mixin import ( + NotificationOperationMixin, +) from kili.adapters.kili_api_gateway.organization.operations_mixin import ( OrganizationOperationMixin, ) @@ -21,6 +24,7 @@ class KiliAPIGateway( CloudStorageOperationMixin, IssueOperationMixin, LabelOperationMixin, + NotificationOperationMixin, OrganizationOperationMixin, ProjectOperationMixin, TagOperationMixin, diff --git a/src/kili/core/graphql/operations/notification/__init__.py b/src/kili/adapters/kili_api_gateway/notification/__init__.py similarity index 100% rename from src/kili/core/graphql/operations/notification/__init__.py rename to src/kili/adapters/kili_api_gateway/notification/__init__.py diff --git a/src/kili/adapters/kili_api_gateway/notification/mappers.py b/src/kili/adapters/kili_api_gateway/notification/mappers.py new file mode 100644 index 000000000..894ac27f9 --- /dev/null +++ b/src/kili/adapters/kili_api_gateway/notification/mappers.py @@ -0,0 +1,15 @@ +"""Mappers for notification API calls.""" + +from typing import Dict + +from kili.adapters.kili_api_gateway.user.mappers import user_where_mapper +from kili.domain.notification import NotificationFilter + + +def map_notification_filter(filters: NotificationFilter) -> Dict: + """Build the GraphQL NotificationWhere variable to be sent in an operation.""" + return { + "hasBeenSeen": filters.has_been_seen, + "id": filters.id, + "user": user_where_mapper(filters.user) if filters.user else None, + } diff --git a/src/kili/adapters/kili_api_gateway/notification/operations.py b/src/kili/adapters/kili_api_gateway/notification/operations.py new file mode 100644 index 000000000..249059292 --- /dev/null +++ b/src/kili/adapters/kili_api_gateway/notification/operations.py @@ -0,0 +1,18 @@ +"""Collection of notification's related GraphQL queries and mutations.""" + +GQL_COUNT_NOTIFICATIONS = """ + query countNotifications($where: NotificationWhere!) { + data: countNotifications(where: $where) + } + """ + + +def get_notifications_query(fragment: str) -> str: + """Get the query for notifications.""" + return f""" + query notifications($where: NotificationWhere!, $first: PageSize!, $skip: Int!) {{ + data: notifications(where: $where, first: $first, skip: $skip) {{ + {fragment} + }} + }} + """ diff --git a/src/kili/adapters/kili_api_gateway/notification/operations_mixin.py b/src/kili/adapters/kili_api_gateway/notification/operations_mixin.py new file mode 100644 index 000000000..19f5980df --- /dev/null +++ b/src/kili/adapters/kili_api_gateway/notification/operations_mixin.py @@ -0,0 +1,35 @@ +"""Mixin extending Kili API Gateway class with notification-related operations.""" + +from typing import Dict, Generator + +from kili.adapters.kili_api_gateway.base import BaseOperationMixin +from kili.adapters.kili_api_gateway.helpers.queries import ( + PaginatedGraphQLQuery, + QueryOptions, + fragment_builder, +) +from kili.domain.notification import NotificationFilter +from kili.domain.types import ListOrTuple + +from .mappers import map_notification_filter +from .operations import GQL_COUNT_NOTIFICATIONS, get_notifications_query + + +class NotificationOperationMixin(BaseOperationMixin): + """GraphQL Mixin extending GraphQL Gateway class with notification-related operations.""" + + def list_notifications( + self, filters: NotificationFilter, fields: ListOrTuple[str], options: QueryOptions + ) -> Generator[Dict, None, None]: + """List notifications.""" + fragment = fragment_builder(fields) + query = get_notifications_query(fragment) + where = map_notification_filter(filters=filters) + return PaginatedGraphQLQuery(self.graphql_client).execute_query_from_paginated_call( + query, where, options, "Retrieving notifications", GQL_COUNT_NOTIFICATIONS + ) + + def count_notification(self, filters: NotificationFilter) -> int: + """Count notifications.""" + variables = {"where": map_notification_filter(filters=filters)} + return self.graphql_client.execute(GQL_COUNT_NOTIFICATIONS, variables)["data"] diff --git a/src/kili/client.py b/src/kili/client.py index c885e6e75..e9c7c7142 100644 --- a/src/kili/client.py +++ b/src/kili/client.py @@ -17,7 +17,6 @@ from kili.entrypoints.mutations.plugins import MutationsPlugins from kili.entrypoints.mutations.project import MutationsProject from kili.entrypoints.mutations.project_version import MutationsProjectVersion -from kili.entrypoints.queries.notification import QueriesNotification from kili.entrypoints.queries.plugins import QueriesPlugins from kili.entrypoints.queries.project_user import QueriesProjectUser from kili.entrypoints.queries.project_version import QueriesProjectVersion @@ -28,6 +27,7 @@ from kili.presentation.client.internal import InternalClientMethods from kili.presentation.client.issue import IssueClientMethods from kili.presentation.client.label import LabelClientMethods +from kili.presentation.client.notification import NotificationClientMethods from kili.presentation.client.organization import OrganizationClientMethods from kili.presentation.client.project import ProjectClientMethods from kili.presentation.client.tag import TagClientMethods @@ -55,7 +55,6 @@ class Kili( # pylint: disable=too-many-ancestors,too-many-instance-attributes MutationsPlugins, MutationsProject, MutationsProjectVersion, - QueriesNotification, QueriesPlugins, QueriesProjectUser, QueriesProjectVersion, @@ -64,6 +63,7 @@ class Kili( # pylint: disable=too-many-ancestors,too-many-instance-attributes CloudStorageClientMethods, IssueClientMethods, LabelClientMethods, + NotificationClientMethods, OrganizationClientMethods, ProjectClientMethods, TagClientMethods, diff --git a/src/kili/core/graphql/operations/notification/queries.py b/src/kili/core/graphql/operations/notification/queries.py deleted file mode 100644 index 22b4ef67d..000000000 --- a/src/kili/core/graphql/operations/notification/queries.py +++ /dev/null @@ -1,51 +0,0 @@ -"""GraphQL Queries of Notifications.""" - -from typing import Optional - -from kili.core.graphql.queries import BaseQueryWhere, GraphQLQuery - - -class NotificationWhere(BaseQueryWhere): - """Tuple to be passed to the NotificationQuery to restrict query.""" - - def __init__( - self, - has_been_seen: Optional[bool] = None, - notification_id: Optional[str] = None, - user_id: Optional[str] = None, - ) -> None: - self.has_been_seen = has_been_seen - self.notification_id = notification_id - self.user_id = user_id - super().__init__() - - def graphql_where_builder(self): - """Build the GraphQL Where payload sent in the resolver from the SDK NotificationWhere.""" - return { - "id": self.notification_id, - "user": { - "id": self.user_id, - }, - "hasBeenSeen": self.has_been_seen, - } - - -class NotificationQuery(GraphQLQuery): - """Notification query.""" - - @staticmethod - def query(fragment): - """Return the GraphQL notifications query.""" - return f""" - query notifications($where: NotificationWhere!, $first: PageSize!, $skip: Int!) {{ - data: notifications(where: $where, first: $first, skip: $skip) {{ - {fragment} - }} - }} - """ - - COUNT_QUERY = """ - query countNotifications($where: NotificationWhere!) { - data: countNotifications(where: $where) - } - """ diff --git a/src/kili/domain/notification.py b/src/kili/domain/notification.py new file mode 100644 index 000000000..0ee8a7862 --- /dev/null +++ b/src/kili/domain/notification.py @@ -0,0 +1,18 @@ +"""Notification domain.""" + +from dataclasses import dataclass +from typing import TYPE_CHECKING, NewType, Optional + +if TYPE_CHECKING: + from .user import UserFilter + +NotificationId = NewType("NotificationId", str) + + +@dataclass +class NotificationFilter: + """Notification filter.""" + + has_been_seen: Optional[bool] + id: Optional[NotificationId] # noqa: A003 + user: Optional["UserFilter"] diff --git a/src/kili/presentation/client/label.py b/src/kili/presentation/client/label.py index 785c49b75..da005c853 100644 --- a/src/kili/presentation/client/label.py +++ b/src/kili/presentation/client/label.py @@ -1,6 +1,5 @@ """Client presentation methods for labels.""" -# pylint: disable=too-many-lines # pylint: disable=too-many-lines import warnings from itertools import repeat diff --git a/src/kili/entrypoints/queries/notification/__init__.py b/src/kili/presentation/client/notification.py similarity index 79% rename from src/kili/entrypoints/queries/notification/__init__.py rename to src/kili/presentation/client/notification.py index e5d434d7a..c6a1a1ecf 100644 --- a/src/kili/entrypoints/queries/notification/__init__.py +++ b/src/kili/presentation/client/notification.py @@ -1,27 +1,25 @@ -"""Notification queries.""" +"""Client presentation methods for notifications.""" from typing import Dict, Generator, Iterable, List, Literal, Optional, overload from typeguard import typechecked from kili.adapters.kili_api_gateway.helpers.queries import QueryOptions -from kili.core.graphql.operations.notification.queries import ( - NotificationQuery, - NotificationWhere, -) +from kili.domain.notification import NotificationFilter, NotificationId from kili.domain.types import ListOrTuple -from kili.entrypoints.base import BaseOperationEntrypointMixin +from kili.domain.user import UserFilter, UserId from kili.presentation.client.helpers.common_validators import ( disable_tqdm_if_as_generator, ) +from kili.use_cases.notification import NotificationUseCases from kili.utils.logcontext import for_all_methods, log_call +from .base import BaseClientMethods -@for_all_methods(log_call, exclude=["__init__"]) -class QueriesNotification(BaseOperationEntrypointMixin): - """Set of Notification queries.""" - # pylint: disable=too-many-arguments +@for_all_methods(log_call, exclude=["__init__"]) +class NotificationClientMethods(BaseClientMethods): + """Methods attached to the Kili client, to run actions on notifications.""" @overload def notifications( @@ -103,17 +101,16 @@ def notifications( Returns: An iterable of notifications. """ - where = NotificationWhere( - has_been_seen=has_been_seen, - notification_id=notification_id, - user_id=user_id, - ) disable_tqdm = disable_tqdm_if_as_generator(as_generator, disable_tqdm) options = QueryOptions(disable_tqdm, first, skip) - notifications_gen = NotificationQuery(self.graphql_client, self.http_client)( - where, fields, options + filters = NotificationFilter( + has_been_seen=has_been_seen, + id=NotificationId(notification_id) if notification_id else None, + user=UserFilter(id=UserId(user_id)) if user_id else None, + ) + notifications_gen = NotificationUseCases(self.kili_api_gateway).list_notifications( + options=options, fields=fields, filters=filters ) - if as_generator: return notifications_gen return list(notifications_gen) @@ -135,9 +132,9 @@ def count_notifications( Returns: The number of notifications with the parameters provided """ - where = NotificationWhere( + filters = NotificationFilter( has_been_seen=has_been_seen, - notification_id=notification_id, - user_id=user_id, + id=NotificationId(notification_id) if notification_id else None, + user=UserFilter(id=UserId(user_id)) if user_id else None, ) - return NotificationQuery(self.graphql_client, self.http_client).count(where) + return NotificationUseCases(self.kili_api_gateway).count_notifications(filters=filters) diff --git a/src/kili/services/label_import/importer/__init__.py b/src/kili/services/label_import/importer/__init__.py index a8df7db3a..2b88adbe2 100644 --- a/src/kili/services/label_import/importer/__init__.py +++ b/src/kili/services/label_import/importer/__init__.py @@ -81,7 +81,7 @@ def process_from_files( # pylint: disable=too-many-arguments self.logger.warning("%d labels have been successfully imported", len(labels)) - def process_from_dict( # pylint: disable=too-many-arguments,too-many-locals + def process_from_dict( # pylint: disable=too-many-arguments self, project_id: Optional[str], labels: List[Dict], diff --git a/src/kili/use_cases/asset/__init__.py b/src/kili/use_cases/asset/__init__.py index 9762ecb3b..f69b27420 100644 --- a/src/kili/use_cases/asset/__init__.py +++ b/src/kili/use_cases/asset/__init__.py @@ -1,7 +1,7 @@ """Asset use cases.""" import itertools -from typing import Dict, Generator, List, Literal, Optional +from typing import Generator, Literal, Optional from kili.adapters.kili_api_gateway.helpers.queries import QueryOptions from kili.core.constants import QUERY_BATCH_SIZE @@ -44,9 +44,10 @@ def list_assets( if download_media_function is not None: # TODO: modify download_media function so it can take a generator of assets - assets_lists: List[List[Dict]] = [] - for assets_batch in batcher(assets_gen, QUERY_BATCH_SIZE): - assets_lists.append(download_media_function(assets_batch)) + assets_lists = [ + download_media_function(assets_batch) + for assets_batch in batcher(assets_gen, QUERY_BATCH_SIZE) + ] assets_gen = (asset for asset in itertools.chain(*assets_lists)) if label_output_format == "parsed_label": diff --git a/src/kili/use_cases/notification/__init__.py b/src/kili/use_cases/notification/__init__.py new file mode 100644 index 000000000..28d11b5cf --- /dev/null +++ b/src/kili/use_cases/notification/__init__.py @@ -0,0 +1,24 @@ +"""Notification use cases.""" + +from typing import Dict, Generator + +from kili.adapters.kili_api_gateway.helpers.queries import QueryOptions +from kili.domain.notification import NotificationFilter +from kili.domain.types import ListOrTuple +from kili.use_cases.base import BaseUseCases + + +class NotificationUseCases(BaseUseCases): + """Notification use cases.""" + + def list_notifications( + self, filters: NotificationFilter, fields: ListOrTuple[str], options: QueryOptions + ) -> Generator[Dict, None, None]: + """List notifications.""" + return self._kili_api_gateway.list_notifications( + filters=filters, fields=fields, options=options + ) + + def count_notifications(self, filters: NotificationFilter) -> int: + """Count notifications.""" + return self._kili_api_gateway.count_notification(filters=filters) diff --git a/tests/integration/presentation/test_notification.py b/tests/integration/presentation/test_notification.py new file mode 100644 index 000000000..c0ab81517 --- /dev/null +++ b/tests/integration/presentation/test_notification.py @@ -0,0 +1,21 @@ +import pytest_mock + +from kili.presentation.client.notification import NotificationClientMethods +from kili.use_cases.notification import NotificationUseCases + + +def test_given_client_when_fetching_notifications_it_works( + mocker: pytest_mock.MockerFixture, kili_api_gateway +): + mocker.patch.object( + NotificationUseCases, "list_notifications", return_value=(n for n in [{"id": "notif_id"}]) + ) + # Given + kili = NotificationClientMethods() + kili.kili_api_gateway = kili_api_gateway + + # When + notifs = kili.notifications() + + # Then + assert notifs == [{"id": "notif_id"}]