diff --git a/orm/topic.py b/orm/topic.py index 85925b4a..9f8598ae 100644 --- a/orm/topic.py +++ b/orm/topic.py @@ -14,6 +14,7 @@ class TopicFollower(Base): created_at = Column(Integer, nullable=False, default=int(time.time())) auto = Column(Boolean, nullable=False, default=False) + class Topic(Base): __tablename__ = 'topic' diff --git a/resolvers/author.py b/resolvers/author.py index 82ea8136..60672628 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -32,10 +32,9 @@ def update_author(_, info, profile): # TODO: caching query @query.field('get_authors_all') def get_authors_all(_, _info): - authors = [] with local_session() as session: authors = session.query(Author).all() - return authors + return authors def count_author_comments_rating(session, author_id) -> int: @@ -96,77 +95,84 @@ def count_author_shouts_rating(session, author_id) -> int: return shouts_likes - shouts_dislikes -def load_author_with_stats(q): - result = get_authors_with_stat(q) - - if result: - [author] = result - with local_session() as session: - comments_count = ( - session.query(Reaction) - .filter( - and_( - Reaction.created_by == author.id, - Reaction.kind == ReactionKind.COMMENT.value, - Reaction.deleted_at.is_(None), - ) +def load_author_ratings(author): + with local_session() as session: + comments_count = ( + session.query(Reaction) + .filter( + and_( + Reaction.created_by == author.id, + Reaction.kind == ReactionKind.COMMENT.value, + Reaction.deleted_at.is_(None), ) - .count() ) - likes_count = ( - session.query(AuthorRating) - .filter( - and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True)) + .count() + ) + likes_count = ( + session.query(AuthorRating) + .filter( + and_(AuthorRating.author == author.id, AuthorRating.plus.is_(True)) + ) + .count() + ) + dislikes_count = ( + session.query(AuthorRating) + .filter( + and_( + AuthorRating.author == author.id, AuthorRating.plus.is_not(True) ) - .count() ) - dislikes_count = ( - session.query(AuthorRating) - .filter( - and_( - AuthorRating.author == author.id, AuthorRating.plus.is_not(True) - ) - ) - .count() - ) - author.stat['rating'] = likes_count - dislikes_count - author.stat['rating_shouts'] = count_author_shouts_rating( - session, author.id - ) - author.stat['rating_comments'] = count_author_comments_rating( - session, author.id - ) - author.stat['commented'] = comments_count - return author + .count() + ) + author.stat['rating'] = likes_count - dislikes_count + author.stat['rating_shouts'] = count_author_shouts_rating( + session, author.id + ) + author.stat['rating_comments'] = count_author_comments_rating( + session, author.id + ) + author.stat['commented'] = comments_count + return author @query.field('get_author') def get_author(_, _info, slug='', author_id=None): q = None - if slug or author_id: - if bool(slug): - q = select(Author).where(Author.slug == slug) - if author_id: - q = select(Author).where(Author.id == author_id) + author = None + try: + if slug or author_id: + if bool(slug): + q = select(Author).where(Author.slug == slug) + if author_id: + q = select(Author).where(Author.id == int(author_id)) - return load_author_with_stats(q) + [author, ] = get_authors_with_stat(q) + author = load_author_ratings(author) + except Exception as exc: + logger.error(exc) + return author async def get_author_by_user_id(user_id: str): redis_key = f'user:{user_id}:author' - res = await redis.execute('GET', redis_key) - if isinstance(res, str): - author = json.loads(res) - if author.get('id'): - logger.debug(f'got cached author: {author}') - return author + author = None + try: + res = await redis.execute('GET', redis_key) + if isinstance(res, str): + author = json.loads(res) + if author.get('id'): + logger.debug(f'got cached author: {author}') + return author - logger.info(f'getting author id for {user_id}') - q = select(Author).filter(Author.user == user_id) - author = load_author_with_stats(q) - if author: + logger.info(f'getting author id for {user_id}') + q = select(Author).filter(Author.user == user_id) + + [author, ] = get_authors_with_stat(q) + author = load_author_ratings(author) update_author(author) - return author + except Exception as exc: + logger.error(exc) + return author @query.field('get_author_id') @@ -186,7 +192,7 @@ def load_authors_by(_, _info, by, limit, offset): q.join(ShoutAuthor) .join(ShoutTopic) .join(Topic) - .where(Topic.slug == by['topic']) + .where(Topic.slug == str(by['topic'])) ) if by.get('last_seen'): # in unix time @@ -219,6 +225,7 @@ def get_author_follows(_, _info, slug='', user=None, author_id=None): else: raise ValueError('Author not found') + @query.field('get_author_follows_topics') def get_author_follows_topics(_, _info, slug='', user=None, author_id=None): with local_session() as session: @@ -290,7 +297,7 @@ def create_author(user_id: str, slug: str, name: str = ''): @query.field('get_author_followers') -def get_author_followers(_, _info, slug): +def get_author_followers(_, _info, slug: str): author_alias = aliased(Author) alias_author_followers = aliased(AuthorFollower) alias_author_authors = aliased(AuthorFollower) @@ -299,9 +306,9 @@ def get_author_followers(_, _info, slug): q = ( select(author_alias) - .join(alias_author_authors, alias_author_authors.follower == author_alias.id) + .join(alias_author_authors, alias_author_authors.follower == int(author_alias.id)) .join( - alias_author_followers, alias_author_followers.author == author_alias.id + alias_author_followers, alias_author_followers.author == int(author_alias.id) ) .filter(author_alias.slug == slug) .add_columns( @@ -311,7 +318,7 @@ def get_author_followers(_, _info, slug): 'followers_stat' ), ) - .outerjoin(alias_shout_author, author_alias.id == alias_shout_author.author) + .outerjoin(alias_shout_author, author_alias.id == int(alias_shout_author.author)) .group_by(author_alias.id) ) diff --git a/resolvers/community.py b/resolvers/community.py index 1b4a6bf7..e7f747ab 100644 --- a/resolvers/community.py +++ b/resolvers/community.py @@ -81,7 +81,7 @@ async def get_communities_all(_, _info): @query.field('get_community') -async def get_community(_, _info, slug): +async def get_community(_, _info, slug: str): q = select(Community).where(Community.slug == slug) q = add_community_stat_columns(q) diff --git a/resolvers/follower.py b/resolvers/follower.py index 169e8365..815f9dc1 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -186,8 +186,8 @@ def author_unfollow(follower_id, slug): def get_topic_followers(_, _info, slug: str, topic_id: int) -> List[Author]: q = select(Author) q = ( - q.join(TopicFollower, TopicFollower.follower == Author.id) - .join(Topic, Topic.id == TopicFollower.topic) + q.join(TopicFollower, TopicFollower.follower == int(Author.id)) + .join(Topic, Topic.id == int(TopicFollower.topic)) .filter(or_(Topic.slug == slug, Topic.id == topic_id)) ) return get_authors_with_stat(q) diff --git a/resolvers/reaction.py b/resolvers/reaction.py index 920fefcd..94a7f601 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -23,17 +23,17 @@ def add_reaction_stat_columns(q, aliased_reaction): q = q.outerjoin(aliased_reaction).add_columns( func.sum(aliased_reaction.id).label('reacted_stat'), func.sum( - case((aliased_reaction.kind == ReactionKind.COMMENT.value, 1), else_=0) + case((aliased_reaction.kind == str(ReactionKind.COMMENT.value), 1), else_=0) ).label('comments_stat'), func.sum( - case((aliased_reaction.kind == ReactionKind.LIKE.value, 1), else_=0) + case((aliased_reaction.kind == str(ReactionKind.LIKE.value), 1), else_=0) ).label('likes_stat'), func.sum( - case((aliased_reaction.kind == ReactionKind.DISLIKE.value, 1), else_=0) + case((aliased_reaction.kind == str(ReactionKind.DISLIKE.value), 1), else_=0) ).label('dislikes_stat'), func.max( case( - (aliased_reaction.kind != ReactionKind.COMMENT.value, None), + (aliased_reaction.kind != str(ReactionKind.COMMENT.value), None), else_=aliased_reaction.created_at, ) ).label('last_comment'), @@ -57,8 +57,7 @@ def check_to_feature(session, approver_id, reaction): """set shout to public if publicated approvers amount > 4""" if not reaction.reply_to and is_positive(reaction.kind): if is_featured_author(session, approver_id): - approvers = [] - approvers.append(approver_id) + approvers = [approver_id, ] # now count how many approvers are voted already reacted_readers = ( session.query(Reaction).where(Reaction.shout == reaction.shout).all() @@ -189,7 +188,7 @@ async def create_reaction(_, info, reaction): ) ) reply_to = reaction.get('reply_to') - if reply_to: + if reply_to and isinstance(reply_to, int): q = q.filter(Reaction.reply_to == reply_to) rating_reactions = session.execute(q).all() same_rating = filter( @@ -352,8 +351,8 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): q = ( select(Reaction, Author, Shout) - .join(Author, Reaction.created_by == Author.id) - .join(Shout, Reaction.shout == Shout.id) + .join(Author, Reaction.created_by == int(Author.id)) + .join(Shout, Reaction.shout == int(Shout.id)) ) # calculate counters @@ -431,7 +430,7 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S # Shouts where follower reacted q2 = ( select(Shout) - .join(Reaction, Reaction.shout_id == Shout.id) + .join(Reaction, Reaction.shout_id == int(Shout.id)) .options(joinedload(Shout.reactions), joinedload(Shout.authors)) .filter(Reaction.created_by == follower_id) .group_by(Shout.id) @@ -442,24 +441,24 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S combined_query = ( union(q1, q2).order_by(desc('last_comment')).limit(limit).offset(offset) ) + results = session.execute(combined_query).scalars() - with local_session() as session: - for [ - shout, - reacted_stat, - commented_stat, - likes_stat, - dislikes_stat, - last_comment, - ] in results: - shout.stat = { - 'viewed': await ViewedStorage.get_shout(shout.slug), - 'rating': int(likes_stat or 0) - int(dislikes_stat or 0), - 'reacted': reacted_stat, - 'commented': commented_stat, - 'last_comment': last_comment, - } - shouts.append(shout) + for [ + shout, + reacted_stat, + commented_stat, + likes_stat, + dislikes_stat, + last_comment, + ] in results: + shout.stat = { + 'viewed': await ViewedStorage.get_shout(shout.slug), + 'rating': int(likes_stat or 0) - int(dislikes_stat or 0), + 'reacted': reacted_stat, + 'commented': commented_stat, + 'last_comment': last_comment, + } + shouts.append(shout) return shouts diff --git a/resolvers/reader.py b/resolvers/reader.py index 7bb4ac24..43192a99 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -207,7 +207,7 @@ async def load_shouts_drafts(_, info): with local_session() as session: reader = session.query(Author).filter(Author.user == user_id).first() if isinstance(reader, Author): - q = q.filter(Shout.created_by == reader.id) + q = q.filter(Shout.created_by == int(reader.id)) q = q.group_by(Shout.id) for [shout] in session.execute(q).unique(): main_topic = ( @@ -240,16 +240,16 @@ async def load_shouts_feed(_, info, options): reader = session.query(Author).filter(Author.user == user_id).first() if reader: reader_followed_authors = select(AuthorFollower.author).where( - AuthorFollower.follower == reader.id + AuthorFollower.follower == int(reader.id) ) reader_followed_topics = select(TopicFollower.topic).where( - TopicFollower.follower == reader.id + TopicFollower.follower == int(reader.id) ) subquery = ( select(Shout.id) - .where(Shout.id == ShoutAuthor.shout) - .where(Shout.id == ShoutTopic.shout) + .where(Shout.id == int(ShoutAuthor.shout)) + .where(Shout.id == int(ShoutTopic.shout)) .where( (ShoutAuthor.author.in_(reader_followed_authors)) | (ShoutTopic.topic.in_(reader_followed_topics)) @@ -430,10 +430,10 @@ async def load_shouts_random_top(_, _info, options): desc( func.sum( case( - (Reaction.kind == ReactionKind.LIKE.value, 1), - (Reaction.kind == ReactionKind.AGREE.value, 1), - (Reaction.kind == ReactionKind.DISLIKE.value, -1), - (Reaction.kind == ReactionKind.DISAGREE.value, -1), + (Reaction.kind == str(ReactionKind.LIKE.value), 1), + (Reaction.kind == str(ReactionKind.AGREE.value), 1), + (Reaction.kind == str(ReactionKind.DISLIKE.value), -1), + (Reaction.kind == str(ReactionKind.DISAGREE.value), -1), else_=0, ) ) @@ -467,8 +467,8 @@ async def load_shouts_random_topic(_, info, limit: int = 10): return {'topic': topic, 'shouts': shouts} return { 'error': 'failed to get random topic after few retries', - shouts: [], - topic: {}, + 'shouts': [], + 'topic': {}, } diff --git a/resolvers/stat.py b/resolvers/stat.py index 578182e6..c3f7cf64 100644 --- a/resolvers/stat.py +++ b/resolvers/stat.py @@ -74,7 +74,7 @@ def author_follows_authors(author_id: int): af = aliased(AuthorFollower, name="af") q = ( select(Author).select_from( - join(Author, af, Author.id == af.author) + join(Author, af, Author.id == int(af.author)) ).where(af.follower == author_id) ) q = add_author_stat_columns(q) @@ -84,7 +84,7 @@ def author_follows_authors(author_id: int): def author_follows_topics(author_id: int): q = ( select(Topic).select_from( - join(Topic, TopicFollower, Topic.id == TopicFollower.topic) + join(Topic, TopicFollower, Topic.id == int(TopicFollower.topic)) ).where(TopicFollower.follower == author_id) ) diff --git a/resolvers/topic.py b/resolvers/topic.py index da75a611..7689b3db 100644 --- a/resolvers/topic.py +++ b/resolvers/topic.py @@ -22,7 +22,7 @@ def get_topics_by_community(_, _info, community_id: int): @query.field('get_topics_by_author') -def get_topics_by_author(_, _info, author_id=None, slug='', user=''): +def get_topics_by_author(_, _info, author_id=0, slug='', user=''): q = select(Topic) if author_id: q = q.join(Author).where(Author.id == author_id) @@ -35,7 +35,7 @@ def get_topics_by_author(_, _info, author_id=None, slug='', user=''): @query.field('get_topic') -def get_topic(_, _info, slug): +def get_topic(_, _info, slug: str): q = select(Topic).filter(Topic.slug == slug) topics = get_topics_with_stat(q) if topics: diff --git a/services/db.py b/services/db.py index f3c02c93..818a00a3 100644 --- a/services/db.py +++ b/services/db.py @@ -22,11 +22,13 @@ REGISTRY: Dict[str, type] = {} # Перехватчики для журнала запросов SQLAlchemy +# noinspection PyUnusedLocal @event.listens_for(Engine, 'before_cursor_execute') def before_cursor_execute(conn, cursor, statement, parameters, context, executemany): conn.query_start_time = time.time() +# noinspection PyUnusedLocal @event.listens_for(Engine, 'after_cursor_execute') def after_cursor_execute(conn, cursor, statement, parameters, context, executemany): if hasattr(conn, '_query_start_time'): @@ -36,6 +38,7 @@ def after_cursor_execute(conn, cursor, statement, parameters, context, executema logger.debug(f"\n{statement}\n{'*' * math.floor(elapsed)} {elapsed:.3f} s") +# noinspection PyUnusedLocal def local_session(src=''): return Session(bind=engine, expire_on_commit=False) diff --git a/services/follows.py b/services/follows.py index a89b6431..27d2c487 100644 --- a/services/follows.py +++ b/services/follows.py @@ -77,7 +77,7 @@ async def update_follows_for_user( await redis.execute('SET', redis_key, json.dumps(follows)) -async def handle_author_follower_change(connection, author_id, follower_id, is_insert): +async def handle_author_follower_change(connection, author_id: int, follower_id: int, is_insert: bool): q = select(Author).filter(Author.id == author_id) authors = get_authors_with_stat(q) author = authors[0] @@ -102,7 +102,7 @@ async def handle_author_follower_change(connection, author_id, follower_id, is_i ) -async def handle_topic_follower_change(connection, topic_id, follower_id, is_insert): +async def handle_topic_follower_change(connection, topic_id: int, follower_id: int, is_insert: bool): q = select(Topic).filter(Topic.id == topic_id) topics = get_topics_with_stat(q) topic = topics[0] diff --git a/services/viewed.py b/services/viewed.py index 19f2e43f..0432932a 100644 --- a/services/viewed.py +++ b/services/viewed.py @@ -65,7 +65,7 @@ class ViewedStorage: logger.info(f' * Миграция проводилась: {self.start_date}') # Запуск фоновой задачи - asyncio.create_task(self.worker()) + _task = asyncio.create_task(self.worker()) else: logger.info(' * Пожалуйста, добавьте ключевой файл Google Analytics') self.disabled = True @@ -84,6 +84,7 @@ class ViewedStorage: except Exception as e: logger.error(f'Ошибка загрузки предварительно подсчитанных просмотров: {e}') + # noinspection PyTypeChecker @staticmethod async def update_pages(): """Запрос всех страниц от Google Analytics, отсортированных по количеству просмотров""" diff --git a/tests/query_follows_test.py b/tests/query_follows_test.py deleted file mode 100644 index c30034c2..00000000 --- a/tests/query_follows_test.py +++ /dev/null @@ -1,78 +0,0 @@ -from unittest.mock import Mock -from resolvers.stat import query_follows - - -def test_query_follows(): - user_id = 'user123' - - # Mocking database session and ORM models - mock_session = Mock() - mock_Author = Mock() - mock_ShoutAuthor = Mock() - mock_AuthorFollower = Mock() - mock_Topic = Mock() - mock_ShoutTopic = Mock() - mock_TopicFollower = Mock() - - # Mocking expected query results - expected_result = { - 'topics': [(1, 5, 10, 15), (2, 8, 12, 20)], # Example topics query result - 'authors': [(101, 3, 6, 9), (102, 4, 7, 11)], # Example authors query result - 'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}], - } - - # Set up mocks to return expected results when queried - mock_session.query().select_from().outerjoin().all.side_effect = [ - expected_result['authors'], # Authors query result - expected_result['topics'], # Topics query result - ] - - # Call the function to test - result = query_follows( - user_id, - session=mock_session, - Author=mock_Author, - ShoutAuthor=mock_ShoutAuthor, - AuthorFollower=mock_AuthorFollower, - Topic=mock_Topic, - ShoutTopic=mock_ShoutTopic, - TopicFollower=mock_TopicFollower, - ) - - # Assertions - assert result['topics'] == expected_result['topics'] - assert result['authors'] == expected_result['authors'] - assert result['communities'] == expected_result['communities'] - - # Assert that mock session was called with expected queries - expected_queries = [ - mock_session.query( - mock_Author.id, - mock_ShoutAuthor.author, - mock_AuthorFollower.author, - mock_AuthorFollower.follower, - ) - .select_from(mock_Author) - .outerjoin(mock_ShoutAuthor, mock_Author.id == mock_ShoutAuthor.author) - .outerjoin(mock_AuthorFollower, mock_Author.id == mock_AuthorFollower.author) - .outerjoin( - mock_AuthorFollower, mock_Author.id == mock_AuthorFollower.follower - ) - .all, - mock_session.query( - mock_Topic.id, - mock_ShoutTopic.topic, - mock_ShoutTopic.topic, - mock_TopicFollower.topic, - ) - .select_from(mock_Topic) - .outerjoin(mock_ShoutTopic, mock_Topic.id == mock_ShoutTopic.topic) - .outerjoin(mock_ShoutTopic, mock_Topic.id == mock_ShoutTopic.topic) - .outerjoin(mock_TopicFollower, mock_Topic.id == mock_TopicFollower.topic) - .all, - ] - mock_session.query.assert_has_calls(expected_queries) - - -# Run the test -test_query_follows()