fixed-fmt-linted
Some checks failed
deploy to v2 / test (push) Failing after 49s
deploy to v2 / deploy (push) Has been skipped

This commit is contained in:
2024-02-17 02:56:15 +03:00
parent 2163e85b16
commit 8b39b47714
17 changed files with 384 additions and 391 deletions

View File

@@ -1,11 +1,13 @@
from orm.notification import Notification
from resolvers.model import NotificationReaction, NotificationAuthor, NotificationShout
from services.db import local_session
from services.rediscache import redis
import asyncio
import logging
logger = logging.getLogger("[listener.listen_task] ")
from orm.notification import Notification
from resolvers.model import NotificationAuthor, NotificationReaction, NotificationShout
from services.db import local_session
from services.rediscache import redis
logger = logging.getLogger('[listener.listen_task] ')
logger.setLevel(logging.DEBUG)
@@ -19,8 +21,8 @@ async def handle_notification(n: ServiceMessage, channel: str):
"""создаеёт новое хранимое уведомление"""
with local_session() as session:
try:
if channel.startswith("follower:"):
author_id = int(channel.split(":")[1])
if channel.startswith('follower:'):
author_id = int(channel.split(':')[1])
if isinstance(n.payload, NotificationAuthor):
n.payload.following_id = author_id
n = Notification(action=n.action, entity=n.entity, payload=n.payload)
@@ -28,7 +30,7 @@ async def handle_notification(n: ServiceMessage, channel: str):
session.commit()
except Exception as e:
session.rollback()
logger.error(f"[listener.handle_reaction] error: {str(e)}")
logger.error(f'[listener.handle_reaction] error: {str(e)}')
async def listen_task(pattern):
@@ -38,9 +40,9 @@ async def listen_task(pattern):
notification_message = ServiceMessage(**message_data)
await handle_notification(notification_message, str(channel))
except Exception as e:
logger.error(f"Error processing notification: {str(e)}")
logger.error(f'Error processing notification: {str(e)}')
async def notifications_worker():
# Use asyncio.gather to run tasks concurrently
await asyncio.gather(listen_task("follower:*"), listen_task("reaction"), listen_task("shout"))
await asyncio.gather(listen_task('follower:*'), listen_task('reaction'), listen_task('shout'))

View File

@@ -1,53 +1,31 @@
import json
import logging
import time
from typing import Dict, List, Tuple, Union
import strawberry
from sqlalchemy import and_, select
from sqlalchemy.orm import aliased
from sqlalchemy.sql import not_
from services.db import local_session
from orm.notification import Notification, NotificationAction, NotificationEntity, NotificationSeen
from resolvers.model import (
NotificationReaction,
NotificationGroup,
NotificationShout,
NotificationAuthor,
NotificationGroup,
NotificationReaction,
NotificationShout,
NotificationsResult,
)
from orm.notification import NotificationAction, NotificationEntity, NotificationSeen, Notification
from typing import Dict, List
import time
import json
import strawberry
from sqlalchemy.orm import aliased
from sqlalchemy import select, and_
import logging
from services.db import local_session
logger = logging.getLogger("[resolvers.schema] ")
logger = logging.getLogger('[resolvers.schema]')
logger.setLevel(logging.DEBUG)
async def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, offset: int = 0):
"""
Retrieves notifications for a given author.
Args:
author_id (int): The ID of the author for whom notifications are retrieved.
after (int, optional): If provided, selects only notifications created after this timestamp will be considered.
limit (int, optional): The maximum number of groupa to retrieve.
offset (int, optional): Offset for pagination
Returns:
Dict[str, NotificationGroup], int, int: A dictionary where keys are thread IDs and values are NotificationGroup objects, unread and total amounts.
This function queries the database to retrieve notifications for the specified author, considering optional filters.
The result is a dictionary where each key is a thread ID, and the corresponding value is a NotificationGroup
containing information about the notifications within that thread.
NotificationGroup structure:
{
entity: str, # Type of entity (e.g., 'reaction', 'shout', 'follower').
updated_at: int, # Timestamp of the latest update in the thread.
shout: Optional[NotificationShout]
reactions: List[int], # List of reaction ids within the thread.
authors: List[NotificationAuthor], # List of authors involved in the thread.
}
"""
NotificationSeenAlias = aliased(NotificationSeen)
query = select(Notification, NotificationSeenAlias.viewer.label("seen")).outerjoin(
def query_notifications(author_id: int, after: int = 0) -> Tuple[int, int, List[Tuple[Notification, bool]]]:
notification_seen_alias = aliased(NotificationSeen)
query = select(Notification, notification_seen_alias.viewer.label('seen')).outerjoin(
NotificationSeen,
and_(NotificationSeen.viewer == author_id, NotificationSeen.notification == Notification.id),
)
@@ -55,17 +33,13 @@ async def get_notifications_grouped(author_id: int, after: int = 0, limit: int =
query = query.filter(Notification.created_at > after)
query = query.group_by(NotificationSeen.notification, Notification.created_at)
groups_amount = 0
unread = 0
total = 0
notifications_by_thread: Dict[str, List[Notification]] = {}
groups_by_thread: Dict[str, NotificationGroup] = {}
with local_session() as session:
total = (
session.query(Notification)
.filter(and_(Notification.action == NotificationAction.CREATE.value, Notification.created_at > after))
.count()
)
unread = (
session.query(Notification)
.filter(
@@ -77,123 +51,140 @@ async def get_notifications_grouped(author_id: int, after: int = 0, limit: int =
)
.count()
)
notifications_result = session.execute(query)
notifications = []
for n, seen in notifications_result:
thread_id = ""
payload = json.loads(n.payload)
logger.debug(f"[resolvers.schema] {n.action} {n.entity}: {payload}")
if n.entity == "shout" and n.action == "create":
shout: NotificationShout = payload
thread_id += f"{shout.id}"
logger.debug(f"create shout: {shout}")
group = groups_by_thread.get(thread_id) or NotificationGroup(
id=thread_id,
entity=n.entity,
shout=shout,
authors=shout.authors,
updated_at=shout.created_at,
reactions=[],
action="create",
seen=author_id in n.seen,
)
# store group in result
groups_by_thread[thread_id] = group
notifications = notifications_by_thread.get(thread_id, [])
if n not in notifications:
notifications.append(n)
notifications_by_thread[thread_id] = notifications
groups_amount += 1
elif n.entity == NotificationEntity.REACTION.value and n.action == NotificationAction.CREATE.value:
reaction: NotificationReaction = payload
shout: NotificationShout = reaction.shout
thread_id += f"{reaction.shout}"
if reaction.kind == "LIKE" or reaction.kind == "DISLIKE":
# TODO: making published reaction vote announce
pass
elif reaction.kind == "COMMENT":
if reaction.reply_to:
thread_id += f"{'::' + str(reaction.reply_to)}"
group: NotificationGroup | None = groups_by_thread.get(thread_id)
notifications: List[Notification] = notifications_by_thread.get(thread_id, [])
if group and notifications:
group.seen = False # any not seen notification make it false
group.shout = shout
group.authors.append(reaction.created_by)
if not group.reactions:
group.reactions = []
group.reactions.append(reaction.id)
# store group in result
groups_by_thread[thread_id] = group
notifications = notifications_by_thread.get(thread_id, [])
if n not in notifications:
notifications.append(n)
notifications_by_thread[thread_id] = notifications
groups_amount += 1
else:
groups_amount += 1
if groups_amount > limit:
break
else:
# init notification group
reactions = []
reactions.append(reaction.id)
group = NotificationGroup(
id=thread_id,
action=n.action,
entity=n.entity,
updated_at=reaction.created_at,
reactions=reactions,
shout=shout,
authors=[
reaction.created_by,
],
seen=author_id in n.seen,
)
# store group in result
groups_by_thread[thread_id] = group
notifications = notifications_by_thread.get(thread_id, [])
if n not in notifications:
notifications.append(n)
notifications_by_thread[thread_id] = notifications
notifications.append((n, seen))
elif n.entity == "follower":
thread_id = "followers"
follower: NotificationAuthor = payload
group = groups_by_thread.get(thread_id) or NotificationGroup(
id=thread_id,
authors=[follower],
updated_at=int(time.time()),
shout=None,
reactions=[],
entity="follower",
action="follow",
seen=author_id in n.seen,
)
group.authors = [
follower,
]
group.updated_at = int(time.time())
# store group in result
return total, unread, notifications
def process_shout_notification(
notification: Notification, seen: bool
) -> Union[Tuple[str, NotificationGroup], None] | None:
if not isinstance(notification.payload, str) or not isinstance(notification.entity, str):
return
payload = json.loads(notification.payload)
shout: NotificationShout = payload
thread_id = str(shout.id)
group = NotificationGroup(
id=thread_id,
entity=notification.entity,
shout=shout,
authors=shout.authors,
updated_at=shout.created_at,
reactions=[],
action='create',
seen=seen,
)
return thread_id, group
def process_reaction_notification(
notification: Notification, seen: bool
) -> Union[Tuple[str, NotificationGroup], None] | None:
if (
not isinstance(notification, Notification)
or not isinstance(notification.payload, str)
or not isinstance(notification.entity, str)
):
return
payload = json.loads(notification.payload)
reaction: NotificationReaction = payload
shout: NotificationShout = reaction.shout
thread_id = str(reaction.shout)
if reaction.kind == 'COMMENT' and reaction.reply_to:
thread_id += f'::{reaction.reply_to}'
group = NotificationGroup(
id=thread_id,
action=str(notification.action),
entity=notification.entity,
updated_at=reaction.created_at,
reactions=[reaction.id],
shout=shout,
authors=[reaction.created_by],
seen=seen,
)
return thread_id, group
def process_follower_notification(
notification: Notification, seen: bool
) -> Union[Tuple[str, NotificationGroup], None] | None:
if not isinstance(notification.payload, str):
return
payload = json.loads(notification.payload)
follower: NotificationAuthor = payload
thread_id = 'followers'
group = NotificationGroup(
id=thread_id,
authors=[follower],
updated_at=int(time.time()),
shout=None,
reactions=[],
entity='follower',
action='follow',
seen=seen,
)
return thread_id, group
async def get_notifications_grouped(
author_id: int, after: int = 0, limit: int = 10
) -> Tuple[Dict[str, NotificationGroup], int, int]:
total, unread, notifications = query_notifications(author_id, after)
groups_by_thread: Dict[str, NotificationGroup] = {}
groups_amount = 0
for notification, seen in notifications:
if groups_amount >= limit:
break
if str(notification.entity) == 'shout' and str(notification.action) == 'create':
result = process_shout_notification(notification, seen)
if result:
thread_id, group = result
groups_by_thread[thread_id] = group
notifications = notifications_by_thread.get(thread_id, [])
if n not in notifications:
notifications.append(n)
notifications_by_thread[thread_id] = notifications
groups_amount += 1
if groups_amount > limit:
break
elif (
str(notification.entity) == NotificationEntity.REACTION.value
and str(notification.action) == NotificationAction.CREATE.value
):
result = process_reaction_notification(notification, seen)
if result:
thread_id, group = result
existing_group = groups_by_thread.get(thread_id)
if existing_group:
existing_group.seen = False
existing_group.shout = group.shout
existing_group.authors.append(group.authors[0])
if not existing_group.reactions:
existing_group.reactions = []
existing_group.reactions.extend(group.reactions or [])
groups_by_thread[thread_id] = existing_group
else:
groups_by_thread[thread_id] = group
groups_amount += 1
return groups_by_thread, notifications_by_thread, unread, total
elif str(notification.entity) == 'follower':
result = process_follower_notification(notification, seen)
if result:
thread_id, group = result
groups_by_thread[thread_id] = group
groups_amount += 1
return groups_by_thread, unread, total
@strawberry.type
class Query:
@strawberry.field
async def load_notifications(self, info, after: int, limit: int = 50, offset: int = 0) -> NotificationsResult:
author_id = info.context.get("author_id")
groups: Dict[str, NotificationGroup] = {}
author_id = info.context.get('author_id')
if author_id:
groups, notifications, total, unread = await get_notifications_grouped(author_id, after, limit, offset)
notifications = sorted(groups.values(), key=lambda group: group.updated_at, reverse=True)
return NotificationsResult(notifications=notifications, total=0, unread=0, error=None)
groups, unread, total = await get_notifications_grouped(author_id, after, limit)
notifications = sorted(groups.values(), key=lambda group: group.updated_at, reverse=True)
return NotificationsResult(notifications=notifications, total=total, unread=unread, error=None)
return NotificationsResult(notifications=[], total=0, unread=0, error=None)

View File

@@ -1,8 +1,11 @@
import strawberry
from typing import List, Optional
import strawberry
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
from orm.notification import Notification as NotificationMessage
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()

View File

@@ -1,11 +1,12 @@
import strawberry
from strawberry.schema.config import StrawberryConfig
from services.auth import LoginRequiredMiddleware
from resolvers.load import Query
from resolvers.seen import Mutation
from services.auth import LoginRequiredMiddleware
from services.db import Base, engine
schema = strawberry.Schema(
query=Query, mutation=Mutation, config=StrawberryConfig(auto_camel_case=False), extensions=[LoginRequiredMiddleware]
)

View File

@@ -1,14 +1,14 @@
from sqlalchemy import and_
from orm.notification import NotificationSeen
from services.db import local_session
from resolvers.model import Notification, NotificationSeenResult, NotificationReaction
import json
import logging
import strawberry
import logging
import json
from sqlalchemy import and_
from sqlalchemy.exc import SQLAlchemyError
from orm.notification import NotificationSeen
from resolvers.model import Notification, NotificationReaction, NotificationSeenResult
from services.db import local_session
logger = logging.getLogger(__name__)
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
class Mutation:
@strawberry.mutation
async def mark_seen(self, info, notification_id: int) -> NotificationSeenResult:
author_id = info.context.get("author_id")
author_id = info.context.get('author_id')
if author_id:
with local_session() as session:
try:
@@ -27,9 +27,9 @@ class Mutation:
except SQLAlchemyError as e:
session.rollback()
logger.error(
f"[mark_notification_as_read] Ошибка при обновлении статуса прочтения уведомления: {str(e)}"
f'[mark_notification_as_read] Ошибка при обновлении статуса прочтения уведомления: {str(e)}'
)
return NotificationSeenResult(error="cant mark as read")
return NotificationSeenResult(error='cant mark as read')
return NotificationSeenResult(error=None)
@strawberry.mutation
@@ -37,7 +37,7 @@ class Mutation:
# TODO: use latest loaded notification_id as input offset parameter
error = None
try:
author_id = info.context.get("author_id")
author_id = info.context.get('author_id')
if author_id:
with local_session() as session:
nnn = session.query(Notification).filter(and_(Notification.created_at > after)).all()
@@ -50,22 +50,22 @@ class Mutation:
session.rollback()
except Exception as e:
print(e)
error = "cant mark as read"
error = 'cant mark as read'
return NotificationSeenResult(error=error)
@strawberry.mutation
async def mark_seen_thread(self, info, thread: str, after: int) -> NotificationSeenResult:
error = None
author_id = info.context.get("author_id")
author_id = info.context.get('author_id')
if author_id:
[shout_id, reply_to_id] = thread.split("::")
[shout_id, reply_to_id] = thread.split('::')
with local_session() as session:
# TODO: handle new follower and new shout notifications
new_reaction_notifications = (
session.query(Notification)
.filter(
Notification.action == "create",
Notification.entity == "reaction",
Notification.action == 'create',
Notification.entity == 'reaction',
Notification.created_at > after,
)
.all()
@@ -73,13 +73,13 @@ class Mutation:
removed_reaction_notifications = (
session.query(Notification)
.filter(
Notification.action == "delete",
Notification.entity == "reaction",
Notification.action == 'delete',
Notification.entity == 'reaction',
Notification.created_at > after,
)
.all()
)
exclude = set([])
exclude = set()
for nr in removed_reaction_notifications:
reaction: NotificationReaction = json.loads(nr.payload)
exclude.add(reaction.id)
@@ -97,5 +97,5 @@ class Mutation:
except Exception:
session.rollback()
else:
error = "You are not logged in"
error = 'You are not logged in'
return NotificationSeenResult(error=error)