From 823e59ea74ab62c9c4171d42445a41bbf16b0ffd Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 21 Feb 2024 19:14:58 +0300 Subject: [PATCH] fmt --- main.py | 14 ++-- orm/author.py | 26 +++---- orm/collection.py | 16 ++--- orm/community.py | 14 ++-- orm/invite.py | 20 +++--- orm/reaction.py | 42 +++++------ orm/shout.py | 52 +++++++------- orm/topic.py | 18 ++--- orm/user.py | 6 +- pyproject.toml | 5 ++ resolvers/__init__.py | 66 ++++++++--------- resolvers/author.py | 98 ++++++++++++------------- resolvers/collab.py | 52 +++++++------- resolvers/community.py | 12 ++-- resolvers/editor.py | 135 +++++++++++++++-------------------- resolvers/follower.py | 54 +++++++------- resolvers/reaction.py | 158 ++++++++++++++++++++--------------------- resolvers/reader.py | 141 ++++++++++++++++-------------------- resolvers/topic.py | 48 ++++++------- server.py | 8 +-- services/auth.py | 85 +++++++++++----------- services/db.py | 24 +++---- services/diff.py | 8 +-- services/follows.py | 80 ++++++++++----------- services/logger.py | 36 +++++----- services/notify.py | 28 ++++---- services/rediscache.py | 8 +-- services/search.py | 99 +++++++++++--------------- services/sentry.py | 2 +- services/unread.py | 4 +- services/viewed.py | 52 +++++++------- services/webhook.py | 18 ++--- settings.py | 20 +++--- 33 files changed, 697 insertions(+), 752 deletions(-) diff --git a/main.py b/main.py index b730a9f2..3a8ac64f 100644 --- a/main.py +++ b/main.py @@ -15,24 +15,24 @@ from services.viewed import ViewedStorage from services.webhook import WebhookEndpoint from settings import DEV_SERVER_PID_FILE_NAME, MODE -import_module("resolvers") -schema = make_executable_schema(load_schema_from_path("schema/"), resolvers) +import_module('resolvers') +schema = make_executable_schema(load_schema_from_path('schema/'), resolvers) async def start(): - if MODE == "development": + if MODE == 'development': if not exists(DEV_SERVER_PID_FILE_NAME): # pid file management - with open(DEV_SERVER_PID_FILE_NAME, "w", encoding="utf-8") as f: + with open(DEV_SERVER_PID_FILE_NAME, 'w', encoding='utf-8') as f: f.write(str(os.getpid())) - print(f"[main] process started in {MODE} mode") + print(f'[main] process started in {MODE} mode') # main starlette app object with ariadne mounted in root app = Starlette( routes=[ - Route("/", GraphQL(schema, debug=True)), - Route("/new-author", WebhookEndpoint), + Route('/', GraphQL(schema, debug=True)), + Route('/new-author', WebhookEndpoint), ], on_startup=[ redis.connect, diff --git a/orm/author.py b/orm/author.py index 94670452..6a8b4d6e 100644 --- a/orm/author.py +++ b/orm/author.py @@ -5,36 +5,36 @@ from services.db import Base class AuthorRating(Base): - __tablename__ = "author_rating" + __tablename__ = 'author_rating' id = None # type: ignore - rater = Column(ForeignKey("author.id"), primary_key=True) - author = Column(ForeignKey("author.id"), primary_key=True) + rater = Column(ForeignKey('author.id'), primary_key=True) + author = Column(ForeignKey('author.id'), primary_key=True) plus = Column(Boolean) class AuthorFollower(Base): - __tablename__ = "author_follower" + __tablename__ = 'author_follower' id = None # type: ignore - follower = Column(ForeignKey("author.id"), primary_key=True) - author = Column(ForeignKey("author.id"), primary_key=True) + follower = Column(ForeignKey('author.id'), primary_key=True) + author = Column(ForeignKey('author.id'), primary_key=True) created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) auto = Column(Boolean, nullable=False, default=False) class Author(Base): - __tablename__ = "author" + __tablename__ = 'author' user = Column(String) # unbounded link with authorizer's User type - name = Column(String, nullable=True, comment="Display name") + name = Column(String, nullable=True, comment='Display name') slug = Column(String, unique=True, comment="Author's slug") - bio = Column(String, nullable=True, comment="Bio") # status description - about = Column(String, nullable=True, comment="About") # long and formatted - pic = Column(String, nullable=True, comment="Picture") - links = Column(JSON, nullable=True, comment="Links") + bio = Column(String, nullable=True, comment='Bio') # status description + about = Column(String, nullable=True, comment='About') # long and formatted + pic = Column(String, nullable=True, comment='Picture') + links = Column(JSON, nullable=True, comment='Links') created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) last_seen = Column(Integer, nullable=False, default=lambda: int(time.time())) updated_at = Column(Integer, nullable=False, default=lambda: int(time.time())) - deleted_at = Column(Integer, nullable=True, comment="Deleted at") + deleted_at = Column(Integer, nullable=True, comment='Deleted at') diff --git a/orm/collection.py b/orm/collection.py index 2b1696d6..87592bc8 100644 --- a/orm/collection.py +++ b/orm/collection.py @@ -6,20 +6,20 @@ from services.db import Base class ShoutCollection(Base): - __tablename__ = "shout_collection" + __tablename__ = 'shout_collection' id = None # type: ignore - shout = Column(ForeignKey("shout.id"), primary_key=True) - collection = Column(ForeignKey("collection.id"), primary_key=True) + shout = Column(ForeignKey('shout.id'), primary_key=True) + collection = Column(ForeignKey('collection.id'), primary_key=True) class Collection(Base): - __tablename__ = "collection" + __tablename__ = 'collection' slug = Column(String, unique=True) - title = Column(String, nullable=False, comment="Title") - body = Column(String, nullable=True, comment="Body") - pic = Column(String, nullable=True, comment="Picture") + title = Column(String, nullable=False, comment='Title') + body = Column(String, nullable=True, comment='Body') + pic = Column(String, nullable=True, comment='Picture') created_at = Column(Integer, default=lambda: int(time.time())) - created_by = Column(ForeignKey("author.id"), comment="Created By") + created_by = Column(ForeignKey('author.id'), comment='Created By') published_at = Column(Integer, default=lambda: int(time.time())) diff --git a/orm/community.py b/orm/community.py index 4674bf5b..1d34b50c 100644 --- a/orm/community.py +++ b/orm/community.py @@ -8,22 +8,22 @@ from orm.author import Author class CommunityAuthor(Base): - __tablename__ = "community_author" + __tablename__ = 'community_author' id = None # type: ignore - author = Column(ForeignKey("author.id"), primary_key=True) - community = Column(ForeignKey("community.id"), primary_key=True) + author = Column(ForeignKey('author.id'), primary_key=True) + community = Column(ForeignKey('community.id'), primary_key=True) joined_at = Column(Integer, nullable=False, default=lambda: int(time.time())) role = Column(String, nullable=False) class Community(Base): - __tablename__ = "community" + __tablename__ = 'community' name = Column(String, nullable=False) slug = Column(String, nullable=False, unique=True) - desc = Column(String, nullable=False, default="") - pic = Column(String, nullable=False, default="") + desc = Column(String, nullable=False, default='') + pic = Column(String, nullable=False, default='') created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) - authors = relationship(Author, secondary="community_author") + authors = relationship(Author, secondary='community_author') diff --git a/orm/invite.py b/orm/invite.py index e6a4d8b3..116ff1a2 100644 --- a/orm/invite.py +++ b/orm/invite.py @@ -7,19 +7,19 @@ from services.db import Base class InviteStatus(Enumeration): - PENDING = "PENDING" - ACCEPTED = "ACCEPTED" - REJECTED = "REJECTED" + PENDING = 'PENDING' + ACCEPTED = 'ACCEPTED' + REJECTED = 'REJECTED' class Invite(Base): - __tablename__ = "invite" + __tablename__ = 'invite' - inviter_id = Column(ForeignKey("author.id"), primary_key=True) - author_id = Column(ForeignKey("author.id"), primary_key=True) - shout_id = Column(ForeignKey("shout.id"), primary_key=True) + inviter_id = Column(ForeignKey('author.id'), primary_key=True) + author_id = Column(ForeignKey('author.id'), primary_key=True) + shout_id = Column(ForeignKey('shout.id'), primary_key=True) status = Column(String, default=InviteStatus.PENDING.value) - inviter = relationship("author", foreign_keys=[inviter_id]) - author = relationship("author", foreign_keys=[author_id]) - shout = relationship("shout") + inviter = relationship('author', foreign_keys=[inviter_id]) + author = relationship('author', foreign_keys=[author_id]) + shout = relationship('shout') diff --git a/orm/reaction.py b/orm/reaction.py index f32b5a5a..cfcc008c 100644 --- a/orm/reaction.py +++ b/orm/reaction.py @@ -10,34 +10,34 @@ class ReactionKind(Enumeration): # TYPE = # rating diff # editor mode - AGREE = "AGREE" # +1 - DISAGREE = "DISAGREE" # -1 - ASK = "ASK" # +0 - PROPOSE = "PROPOSE" # +0 - PROOF = "PROOF" # +1 - DISPROOF = "DISPROOF" # -1 - ACCEPT = "ACCEPT" # +1 - REJECT = "REJECT" # -1 + AGREE = 'AGREE' # +1 + DISAGREE = 'DISAGREE' # -1 + ASK = 'ASK' # +0 + PROPOSE = 'PROPOSE' # +0 + PROOF = 'PROOF' # +1 + DISPROOF = 'DISPROOF' # -1 + ACCEPT = 'ACCEPT' # +1 + REJECT = 'REJECT' # -1 # public feed - QUOTE = "QUOTE" # +0 TODO: use to bookmark in collection - COMMENT = "COMMENT" # +0 - LIKE = "LIKE" # +1 - DISLIKE = "DISLIKE" # -1 + QUOTE = 'QUOTE' # +0 TODO: use to bookmark in collection + COMMENT = 'COMMENT' # +0 + LIKE = 'LIKE' # +1 + DISLIKE = 'DISLIKE' # -1 class Reaction(Base): - __tablename__ = "reaction" + __tablename__ = 'reaction' - body = Column(String, default="", comment="Reaction Body") + body = Column(String, default='', comment='Reaction Body') created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) - updated_at = Column(Integer, nullable=True, comment="Updated at") - deleted_at = Column(Integer, nullable=True, comment="Deleted at") - deleted_by = Column(ForeignKey("author.id"), nullable=True) - reply_to = Column(ForeignKey("reaction.id"), nullable=True) - quote = Column(String, nullable=True, comment="Original quoted text") - shout = Column(ForeignKey("shout.id"), nullable=False) - created_by = Column(ForeignKey("author.id"), nullable=False) + updated_at = Column(Integer, nullable=True, comment='Updated at') + deleted_at = Column(Integer, nullable=True, comment='Deleted at') + deleted_by = Column(ForeignKey('author.id'), nullable=True) + reply_to = Column(ForeignKey('reaction.id'), nullable=True) + quote = Column(String, nullable=True, comment='Original quoted text') + shout = Column(ForeignKey('shout.id'), nullable=False) + created_by = Column(ForeignKey('author.id'), nullable=False) kind = Column(String, nullable=False) oid = Column(String) diff --git a/orm/shout.py b/orm/shout.py index 1454644e..bba8acb8 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -11,44 +11,44 @@ from orm.topic import Topic class ShoutTopic(Base): - __tablename__ = "shout_topic" + __tablename__ = 'shout_topic' id = None # type: ignore - shout = Column(ForeignKey("shout.id"), primary_key=True) - topic = Column(ForeignKey("topic.id"), primary_key=True) + shout = Column(ForeignKey('shout.id'), primary_key=True) + topic = Column(ForeignKey('topic.id'), primary_key=True) main = Column(Boolean, nullable=True) class ShoutReactionsFollower(Base): - __tablename__ = "shout_reactions_followers" + __tablename__ = 'shout_reactions_followers' id = None # type: ignore - follower = Column(ForeignKey("author.id"), primary_key=True) - shout = Column(ForeignKey("shout.id"), primary_key=True) + follower = Column(ForeignKey('author.id'), primary_key=True) + shout = Column(ForeignKey('shout.id'), primary_key=True) auto = Column(Boolean, nullable=False, default=False) created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) deleted_at = Column(Integer, nullable=True) class ShoutAuthor(Base): - __tablename__ = "shout_author" + __tablename__ = 'shout_author' id = None # type: ignore - shout = Column(ForeignKey("shout.id"), primary_key=True) - author = Column(ForeignKey("author.id"), primary_key=True) - caption = Column(String, nullable=True, default="") + shout = Column(ForeignKey('shout.id'), primary_key=True) + author = Column(ForeignKey('author.id'), primary_key=True) + caption = Column(String, nullable=True, default='') class ShoutCommunity(Base): - __tablename__ = "shout_community" + __tablename__ = 'shout_community' id = None # type: ignore - shout = Column(ForeignKey("shout.id"), primary_key=True) - community = Column(ForeignKey("community.id"), primary_key=True) + shout = Column(ForeignKey('shout.id'), primary_key=True) + community = Column(ForeignKey('community.id'), primary_key=True) class Shout(Base): - __tablename__ = "shout" + __tablename__ = 'shout' created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) updated_at = Column(Integer, nullable=True) @@ -56,28 +56,28 @@ class Shout(Base): featured_at = Column(Integer, nullable=True) deleted_at = Column(Integer, nullable=True) - created_by = Column(ForeignKey("author.id"), nullable=False) - updated_by = Column(ForeignKey("author.id"), nullable=True) - deleted_by = Column(ForeignKey("author.id"), nullable=True) + created_by = Column(ForeignKey('author.id'), nullable=False) + updated_by = Column(ForeignKey('author.id'), nullable=True) + deleted_by = Column(ForeignKey('author.id'), nullable=True) - body = Column(String, nullable=False, comment="Body") + body = Column(String, nullable=False, comment='Body') slug = Column(String, unique=True) - cover = Column(String, nullable=True, comment="Cover image url") - cover_caption = Column(String, nullable=True, comment="Cover image alt caption") + cover = Column(String, nullable=True, comment='Cover image url') + cover_caption = Column(String, nullable=True, comment='Cover image alt caption') lead = Column(String, nullable=True) description = Column(String, nullable=True) title = Column(String, nullable=False) subtitle = Column(String, nullable=True) - layout = Column(String, nullable=False, default="article") + layout = Column(String, nullable=False, default='article') media = Column(JSON, nullable=True) - authors = relationship(Author, secondary="shout_author") - topics = relationship(Topic, secondary="shout_topic") - communities = relationship(Community, secondary="shout_community") + authors = relationship(Author, secondary='shout_author') + topics = relationship(Topic, secondary='shout_topic') + communities = relationship(Community, secondary='shout_community') reactions = relationship(Reaction) - lang = Column(String, nullable=False, default="ru", comment="Language") - version_of = Column(ForeignKey("shout.id"), nullable=True) + lang = Column(String, nullable=False, default='ru', comment='Language') + version_of = Column(ForeignKey('shout.id'), nullable=True) oid = Column(String, nullable=True) seo = Column(String, nullable=True) # JSON diff --git a/orm/topic.py b/orm/topic.py index 93583ad9..a4b2826f 100644 --- a/orm/topic.py +++ b/orm/topic.py @@ -6,21 +6,21 @@ from services.db import Base class TopicFollower(Base): - __tablename__ = "topic_followers" + __tablename__ = 'topic_followers' id = None # type: ignore - follower = Column(ForeignKey("author.id"), primary_key=True) - topic = Column(ForeignKey("topic.id"), primary_key=True) + follower = Column(ForeignKey('author.id'), primary_key=True) + topic = Column(ForeignKey('topic.id'), primary_key=True) created_at = Column(Integer, nullable=False, default=lambda: int(time.time())) auto = Column(Boolean, nullable=False, default=False) class Topic(Base): - __tablename__ = "topic" + __tablename__ = 'topic' slug = Column(String, unique=True) - title = Column(String, nullable=False, comment="Title") - body = Column(String, nullable=True, comment="Body") - pic = Column(String, nullable=True, comment="Picture") - community = Column(ForeignKey("community.id"), default=1) - oid = Column(String, nullable=True, comment="Old ID") + title = Column(String, nullable=False, comment='Title') + body = Column(String, nullable=True, comment='Body') + pic = Column(String, nullable=True, comment='Picture') + community = Column(ForeignKey('community.id'), default=1) + oid = Column(String, nullable=True, comment='Old ID') diff --git a/orm/user.py b/orm/user.py index 550813e6..6001b2ea 100644 --- a/orm/user.py +++ b/orm/user.py @@ -6,7 +6,7 @@ from services.db import Base class User(Base): - __tablename__ = "authorizer_users" + __tablename__ = 'authorizer_users' id = Column(String, primary_key=True, unique=True, nullable=False, default=None) key = Column(String) @@ -24,7 +24,7 @@ class User(Base): # preferred_username = Column(String, nullable=False) picture = Column(String) revoked_timestamp = Column(Integer) - roles = Column(String, default="author, reader") - signup_methods = Column(String, default="magic_link_login") + roles = Column(String, default='author, reader') + signup_methods = Column(String, default='magic_link_login') created_at = Column(Integer, default=lambda: int(time.time())) updated_at = Column(Integer, default=lambda: int(time.time())) diff --git a/pyproject.toml b/pyproject.toml index 7c17ae01..2bcf5e53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,11 @@ ignore = [] line-length = 120 target-version = "py312" +[tool.ruff.format] +# Prefer single quotes over double quotes. +quote-style = "single" +skip-magic-trailing-comma = true + [tool.pyright] venvPath = "." venv = ".venv" diff --git a/resolvers/__init__.py b/resolvers/__init__.py index e31ad198..3e3e2710 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -42,43 +42,43 @@ from resolvers.topic import ( __all__ = [ # author - "get_author", - "get_author_id", - "get_author_follows", - "get_authors_all", - "load_authors_by", - "rate_author", - "update_author", + 'get_author', + 'get_author_id', + 'get_author_follows', + 'get_authors_all', + 'load_authors_by', + 'rate_author', + 'update_author', # community - "get_community", - "get_communities_all", + 'get_community', + 'get_communities_all', # topic - "get_topic", - "get_topics_all", - "get_topics_by_community", - "get_topics_by_author", + 'get_topic', + 'get_topics_all', + 'get_topics_by_community', + 'get_topics_by_author', # reader - "get_shout", - "load_shouts_by", - "load_shouts_feed", - "load_shouts_search", - "load_shouts_followed", - "load_shouts_unrated", - "load_shouts_random_top", - "load_shouts_random_topic", + 'get_shout', + 'load_shouts_by', + 'load_shouts_feed', + 'load_shouts_search', + 'load_shouts_followed', + 'load_shouts_unrated', + 'load_shouts_random_top', + 'load_shouts_random_topic', # follower - "follow", - "unfollow", - "get_topic_followers", - "get_shout_followers", - "get_author_followers", + 'follow', + 'unfollow', + 'get_topic_followers', + 'get_shout_followers', + 'get_author_followers', # editor - "create_shout", - "update_shout", - "delete_shout", + 'create_shout', + 'update_shout', + 'delete_shout', # reaction - "create_reaction", - "update_reaction", - "delete_reaction", - "load_reactions_by", + 'create_reaction', + 'update_reaction', + 'delete_reaction', + 'load_reactions_by', ] diff --git a/resolvers/author.py b/resolvers/author.py index 8733e613..262881e5 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -21,18 +21,18 @@ from services.logger import root_logger as logger def add_author_stat_columns(q): shout_author_aliased = aliased(ShoutAuthor) q = q.outerjoin(shout_author_aliased).add_columns( - func.count(distinct(shout_author_aliased.shout)).label("shouts_stat") + func.count(distinct(shout_author_aliased.shout)).label('shouts_stat') ) followers_table = aliased(AuthorFollower) q = q.outerjoin(followers_table, followers_table.author == Author.id).add_columns( - func.count(distinct(followers_table.follower)).label("followers_stat") + func.count(distinct(followers_table.follower)).label('followers_stat') ) followings_table = aliased(AuthorFollower) q = q.outerjoin( followings_table, followings_table.follower == Author.id - ).add_columns(func.count(distinct(followers_table.author)).label("followings_stat")) + ).add_columns(func.count(distinct(followers_table.author)).label('followings_stat')) q = q.group_by(Author.id) return q @@ -45,29 +45,29 @@ async def get_authors_from_query(q): q ): author.stat = { - "shouts": shouts_stat, - "viewed": await ViewedStorage.get_author(author.slug), - "followers": followers_stat, - "followings": followings_stat, + 'shouts': shouts_stat, + 'viewed': await ViewedStorage.get_author(author.slug), + 'followers': followers_stat, + 'followings': followings_stat, } authors.append(author) return authors -@mutation.field("update_author") +@mutation.field('update_author') @login_required async def update_author(_, info, profile): - user_id = info.context["user_id"] + user_id = info.context['user_id'] with local_session() as session: author = session.query(Author).where(Author.user == user_id).first() Author.update(author, profile) session.add(author) session.commit() - return {"error": None, "author": author} + return {'error': None, 'author': author} # TODO: caching query -@query.field("get_authors_all") +@query.field('get_authors_all') async def get_authors_all(_, _info): authors = [] with local_session() as session: @@ -168,19 +168,19 @@ async def load_author_with_stats(q): ) .count() ) - author.stat["rating"] = likes_count - dislikes_count - author.stat["rating_shouts"] = count_author_shouts_rating( + 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( + author.stat['rating_comments'] = count_author_comments_rating( session, author.id ) - author.stat["commented"] = comments_count + author.stat['commented'] = comments_count return author -@query.field("get_author") -async def get_author(_, _info, slug="", author_id=None): +@query.field('get_author') +async def get_author(_, _info, slug='', author_id=None): q = None if slug or author_id: if bool(slug): @@ -192,64 +192,64 @@ async def get_author(_, _info, slug="", author_id=None): async def get_author_by_user_id(user_id: str): - redis_key = f"user:{user_id}:author" - res = await redis.execute("GET", redis_key) + 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}") + if author.get('id'): + logger.debug(f'got cached author: {author}') return author - logger.info(f"getting author id for {user_id}") + logger.info(f'getting author id for {user_id}') q = select(Author).filter(Author.user == user_id) author = await load_author_with_stats(q) if author: await redis.execute( - "set", + 'set', redis_key, json.dumps( { - "id": author.id, - "name": author.name, - "slug": author.slug, - "pic": author.pic, + 'id': author.id, + 'name': author.name, + 'slug': author.slug, + 'pic': author.pic, } ), ) return author -@query.field("get_author_id") +@query.field('get_author_id') async def get_author_id(_, _info, user: str): return await get_author_by_user_id(user) -@query.field("load_authors_by") +@query.field('load_authors_by') async def load_authors_by(_, _info, by, limit, offset): q = select(Author) q = add_author_stat_columns(q) - if by.get("slug"): + if by.get('slug'): q = q.filter(Author.slug.ilike(f"%{by['slug']}%")) - elif by.get("name"): + elif by.get('name'): q = q.filter(Author.name.ilike(f"%{by['name']}%")) - elif by.get("topic"): + elif by.get('topic'): q = ( q.join(ShoutAuthor) .join(ShoutTopic) .join(Topic) - .where(Topic.slug == by["topic"]) + .where(Topic.slug == by['topic']) ) - if by.get("last_seen"): # in unix time - before = int(time.time()) - by["last_seen"] + if by.get('last_seen'): # in unix time + before = int(time.time()) - by['last_seen'] q = q.filter(Author.last_seen > before) - elif by.get("created_at"): # in unix time - before = int(time.time()) - by["created_at"] + elif by.get('created_at'): # in unix time + before = int(time.time()) - by['created_at'] q = q.filter(Author.created_at > before) - order = by.get("order") - if order == "followers" or order == "shouts": - q = q.order_by(desc(f"{order}_stat")) + order = by.get('order') + if order == 'followers' or order == 'shouts': + q = q.order_by(desc(f'{order}_stat')) q = q.limit(limit).offset(offset) @@ -258,9 +258,9 @@ async def load_authors_by(_, _info, by, limit, offset): return authors -@query.field("get_author_follows") +@query.field('get_author_follows') async def get_author_follows( - _, _info, slug="", user=None, author_id=None + _, _info, slug='', user=None, author_id=None ) -> List[Author]: user_id = user if not user_id and author_id or slug: @@ -277,13 +277,13 @@ async def get_author_follows( return follows else: - raise ValueError("Author not found") + raise ValueError('Author not found') -@mutation.field("rate_author") +@mutation.field('rate_author') @login_required async def rate_author(_, info, rated_slug, value): - user_id = info.context["user_id"] + user_id = info.context['user_id'] with local_session() as session: rated_author = session.query(Author).filter(Author.slug == rated_slug).first() @@ -312,19 +312,19 @@ async def rate_author(_, info, rated_slug, value): session.add(rating) session.commit() except Exception as err: - return {"error": err} + return {'error': err} return {} -async def create_author(user_id: str, slug: str, name: str = ""): +async def create_author(user_id: str, slug: str, name: str = ''): with local_session() as session: new_author = Author(user=user_id, slug=slug, name=name) session.add(new_author) session.commit() - logger.info(f"author created by webhook {new_author.dict()}") + logger.info(f'author created by webhook {new_author.dict()}') -@query.field("get_author_followers") +@query.field('get_author_followers') async def get_author_followers(_, _info, slug) -> List[Author]: q = select(Author) q = add_author_stat_columns(q) diff --git a/resolvers/collab.py b/resolvers/collab.py index c22c72da..90db98c2 100644 --- a/resolvers/collab.py +++ b/resolvers/collab.py @@ -6,10 +6,10 @@ from services.db import local_session from services.schema import mutation -@mutation.field("accept_invite") +@mutation.field('accept_invite') @login_required async def accept_invite(_, info, invite_id: int): - user_id = info.context["user_id"] + user_id = info.context['user_id'] # Check if the user exists with local_session() as session: @@ -30,19 +30,19 @@ async def accept_invite(_, info, invite_id: int): session.delete(invite) session.add(shout) session.commit() - return {"success": True, "message": "Invite accepted"} + return {'success': True, 'message': 'Invite accepted'} else: - return {"error": "Shout not found"} + return {'error': 'Shout not found'} else: - return {"error": "Invalid invite or already accepted/rejected"} + return {'error': 'Invalid invite or already accepted/rejected'} else: - return {"error": "User not found"} + return {'error': 'User not found'} -@mutation.field("reject_invite") +@mutation.field('reject_invite') @login_required async def reject_invite(_, info, invite_id: int): - user_id = info.context["user_id"] + user_id = info.context['user_id'] # Check if the user exists with local_session() as session: @@ -58,17 +58,17 @@ async def reject_invite(_, info, invite_id: int): # Delete the invite session.delete(invite) session.commit() - return {"success": True, "message": "Invite rejected"} + return {'success': True, 'message': 'Invite rejected'} else: - return {"error": "Invalid invite or already accepted/rejected"} + return {'error': 'Invalid invite or already accepted/rejected'} else: - return {"error": "User not found"} + return {'error': 'User not found'} -@mutation.field("create_invite") +@mutation.field('create_invite') @login_required -async def create_invite(_, info, slug: str = "", author_id: int = 0): - user_id = info.context["user_id"] +async def create_invite(_, info, slug: str = '', author_id: int = 0): + user_id = info.context['user_id'] # Check if the inviter is the owner of the shout with local_session() as session: @@ -90,7 +90,7 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0): .first() ) if existing_invite: - return {"error": "Invite already sent"} + return {'error': 'Invite already sent'} # Create a new invite new_invite = Invite( @@ -102,17 +102,17 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0): session.add(new_invite) session.commit() - return {"error": None, "invite": new_invite} + return {'error': None, 'invite': new_invite} else: - return {"error": "Invalid author"} + return {'error': 'Invalid author'} else: - return {"error": "Access denied"} + return {'error': 'Access denied'} -@mutation.field("remove_author") +@mutation.field('remove_author') @login_required -async def remove_author(_, info, slug: str = "", author_id: int = 0): - user_id = info.context["user_id"] +async def remove_author(_, info, slug: str = '', author_id: int = 0): + user_id = info.context['user_id'] with local_session() as session: author = session.query(Author).filter(Author.user == user_id).first() if author: @@ -124,13 +124,13 @@ async def remove_author(_, info, slug: str = "", author_id: int = 0): ] session.commit() return {} - return {"error": "Access denied"} + return {'error': 'Access denied'} -@mutation.field("remove_invite") +@mutation.field('remove_invite') @login_required async def remove_invite(_, info, invite_id: int): - user_id = info.context["user_id"] + user_id = info.context['user_id'] # Check if the user exists with local_session() as session: @@ -148,6 +148,6 @@ async def remove_invite(_, info, invite_id: int): session.commit() return {} else: - return {"error": "Invalid invite or already accepted/rejected"} + return {'error': 'Invalid invite or already accepted/rejected'} else: - return {"error": "Author not found"} + return {'error': 'Author not found'} diff --git a/resolvers/community.py b/resolvers/community.py index c1efa61c..1b4a6bf7 100644 --- a/resolvers/community.py +++ b/resolvers/community.py @@ -14,12 +14,12 @@ def add_community_stat_columns(q): shout_community_aliased = aliased(ShoutCommunity) q = q.outerjoin(shout_community_aliased).add_columns( - func.count(distinct(shout_community_aliased.shout)).label("shouts_stat") + func.count(distinct(shout_community_aliased.shout)).label('shouts_stat') ) q = q.outerjoin( community_followers, community_followers.author == Author.id ).add_columns( - func.count(distinct(community_followers.follower)).label("followers_stat") + func.count(distinct(community_followers.follower)).label('followers_stat') ) q = q.group_by(Author.id) @@ -32,8 +32,8 @@ def get_communities_from_query(q): with local_session() as session: for [c, shouts_stat, followers_stat] in session.execute(q): c.stat = { - "shouts": shouts_stat, - "followers": followers_stat, + 'shouts': shouts_stat, + 'followers': followers_stat, # "commented": commented_stat, } ccc.append(c) @@ -72,7 +72,7 @@ def community_unfollow(follower_id, slug): return False -@query.field("get_communities_all") +@query.field('get_communities_all') async def get_communities_all(_, _info): q = select(Author) q = add_community_stat_columns(q) @@ -80,7 +80,7 @@ async def get_communities_all(_, _info): return get_communities_from_query(q) -@query.field("get_community") +@query.field('get_community') async def get_community(_, _info, slug): q = select(Community).where(Community.slug == slug) q = add_community_stat_columns(q) diff --git a/resolvers/editor.py b/resolvers/editor.py index 9e133cba..b28ad78e 100644 --- a/resolvers/editor.py +++ b/resolvers/editor.py @@ -18,20 +18,17 @@ from services.search import search_service from services.logger import root_logger as logger -@query.field("get_shouts_drafts") +@query.field('get_shouts_drafts') @login_required async def get_shouts_drafts(_, info): - user_id = info.context["user_id"] + user_id = info.context['user_id'] shouts = [] with local_session() as session: author = session.query(Author).filter(Author.user == user_id).first() if author: q = ( select(Shout) - .options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) .filter(and_(Shout.deleted_at.is_(None), Shout.created_by == author.id)) .filter(Shout.published_at.is_(None)) .group_by(Shout.id) @@ -40,28 +37,28 @@ async def get_shouts_drafts(_, info): return shouts -@mutation.field("create_shout") +@mutation.field('create_shout') @login_required async def create_shout(_, info, inp): - user_id = info.context["user_id"] + user_id = info.context['user_id'] with local_session() as session: author = session.query(Author).filter(Author.user == user_id).first() if isinstance(author, Author): current_time = int(time.time()) - slug = inp.get("slug") or f"draft-{current_time}" + slug = inp.get('slug') or f'draft-{current_time}' shout_dict = { - "title": inp.get("title", ""), - "subtitle": inp.get("subtitle", ""), - "lead": inp.get("lead", ""), - "description": inp.get("description", ""), - "body": inp.get("body", ""), - "layout": inp.get("layout", "article"), - "created_by": author.id, - "authors": [], - "slug": slug, - "topics": inp.get("topics", []), - "published_at": None, - "created_at": current_time, # Set created_at as Unix timestamp + 'title': inp.get('title', ''), + 'subtitle': inp.get('subtitle', ''), + 'lead': inp.get('lead', ''), + 'description': inp.get('description', ''), + 'body': inp.get('body', ''), + 'layout': inp.get('layout', 'article'), + 'created_by': author.id, + 'authors': [], + 'slug': slug, + 'topics': inp.get('topics', []), + 'published_at': None, + 'created_at': current_time, # Set created_at as Unix timestamp } new_shout = Shout(**shout_dict) @@ -77,7 +74,7 @@ async def create_shout(_, info, inp): topics = ( session.query(Topic) - .filter(Topic.slug.in_(inp.get("topics", []))) + .filter(Topic.slug.in_(inp.get('topics', []))) .all() ) for topic in topics: @@ -89,20 +86,15 @@ async def create_shout(_, info, inp): # notifier # await notify_shout(shout_dict, 'create') - return {"shout": shout.dict()} + return {'shout': shout.dict()} - return {"error": "cant create shout"} + return {'error': 'cant create shout'} def patch_main_topic(session, main_topic, shout): old_main_topic = ( session.query(ShoutTopic) - .filter( - and_( - ShoutTopic.shout == shout.id, - ShoutTopic.main.is_(True), - ) - ) + .filter(and_(ShoutTopic.shout == shout.id, ShoutTopic.main.is_(True))) .first() ) @@ -112,25 +104,22 @@ def patch_main_topic(session, main_topic, shout): new_main_topic = ( session.query(ShoutTopic) .filter( - and_( - ShoutTopic.shout == shout.id, - ShoutTopic.topic == main_topic.id, - ) + and_(ShoutTopic.shout == shout.id, ShoutTopic.topic == main_topic.id) ) .first() ) if old_main_topic and new_main_topic and old_main_topic is not new_main_topic: - ShoutTopic.update(old_main_topic, {"main": False}) + ShoutTopic.update(old_main_topic, {'main': False}) session.add(old_main_topic) - ShoutTopic.update(new_main_topic, {"main": True}) + ShoutTopic.update(new_main_topic, {'main': True}) session.add(new_main_topic) def patch_topics(session, shout, topics_input): new_topics_to_link = [ - Topic(**new_topic) for new_topic in topics_input if new_topic["id"] < 0 + Topic(**new_topic) for new_topic in topics_input if new_topic['id'] < 0 ] if new_topics_to_link: session.add_all(new_topics_to_link) @@ -141,12 +130,12 @@ def patch_topics(session, shout, topics_input): session.add(created_unlinked_topic) existing_topics_input = [ - topic_input for topic_input in topics_input if topic_input.get("id", 0) > 0 + topic_input for topic_input in topics_input if topic_input.get('id', 0) > 0 ] existing_topic_to_link_ids = [ - existing_topic_input["id"] + existing_topic_input['id'] for existing_topic_input in existing_topics_input - if existing_topic_input["id"] not in [topic.id for topic in shout.topics] + if existing_topic_input['id'] not in [topic.id for topic in shout.topics] ] for existing_topic_to_link_id in existing_topic_to_link_ids: @@ -158,60 +147,54 @@ def patch_topics(session, shout, topics_input): topic_to_unlink_ids = [ topic.id for topic in shout.topics - if topic.id not in [topic_input["id"] for topic_input in existing_topics_input] + if topic.id not in [topic_input['id'] for topic_input in existing_topics_input] ] session.query(ShoutTopic).filter( - and_( - ShoutTopic.shout == shout.id, - ShoutTopic.topic.in_(topic_to_unlink_ids), - ) + and_(ShoutTopic.shout == shout.id, ShoutTopic.topic.in_(topic_to_unlink_ids)) ).delete(synchronize_session=False) -@mutation.field("update_shout") +@mutation.field('update_shout') @login_required async def update_shout(_, info, shout_id, shout_input=None, publish=False): - user_id = info.context["user_id"] - roles = info.context["roles"] + user_id = info.context['user_id'] + roles = info.context['roles'] shout_input = shout_input or {} with local_session() as session: author = session.query(Author).filter(Author.user == user_id).first() current_time = int(time.time()) - shout_id = shout_id or shout_input.get("id") + shout_id = shout_id or shout_input.get('id') if isinstance(author, Author) and isinstance(shout_id, int): shout = ( session.query(Shout) - .options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) .filter(Shout.id == shout_id) .first() ) if not shout: - return {"error": "shout not found"} + return {'error': 'shout not found'} if ( shout.created_by is not author.id and author.id not in shout.authors - and "editor" not in roles + and 'editor' not in roles ): - return {"error": "access denied"} + return {'error': 'access denied'} # topics patch - topics_input = shout_input.get("topics") + topics_input = shout_input.get('topics') if topics_input: patch_topics(session, shout, topics_input) - del shout_input["topics"] + del shout_input['topics'] # main topic - main_topic = shout_input.get("main_topic") + main_topic = shout_input.get('main_topic') if main_topic: patch_main_topic(session, main_topic, shout) - shout_input["updated_at"] = current_time - shout_input["published_at"] = current_time if publish else None + shout_input['updated_at'] = current_time + shout_input['published_at'] = current_time if publish else None Shout.update(shout, shout_input) session.add(shout) session.commit() @@ -219,44 +202,44 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False): shout_dict = shout.dict() if not publish: - await notify_shout(shout_dict, "update") + await notify_shout(shout_dict, 'update') else: - await notify_shout(shout_dict, "published") + await notify_shout(shout_dict, 'published') # search service indexing search_service.index(shout) - return {"shout": shout_dict} - logger.debug(f" cannot update with data: {shout_input}") - return {"error": "cant update shout"} + return {'shout': shout_dict} + logger.debug(f' cannot update with data: {shout_input}') + return {'error': 'cant update shout'} -@mutation.field("delete_shout") +@mutation.field('delete_shout') @login_required async def delete_shout(_, info, shout_id): - user_id = info.context["user_id"] - roles = info.context["roles"] + user_id = info.context['user_id'] + roles = info.context['roles'] with local_session() as session: author = session.query(Author).filter(Author.user == user_id).first() shout = session.query(Shout).filter(Shout.id == shout_id).first() if not shout: - return {"error": "invalid shout id"} + return {'error': 'invalid shout id'} if author and shout: if ( shout.created_by is not author.id and author.id not in shout.authors - and "editor" not in roles + and 'editor' not in roles ): - return {"error": "access denied"} + return {'error': 'access denied'} for author_id in shout.authors: reactions_unfollow(author_id, shout_id) shout_dict = shout.dict() - shout_dict["deleted_at"] = int(time.time()) + shout_dict['deleted_at'] = int(time.time()) Shout.update(shout, shout_dict) session.add(shout) session.commit() - await notify_shout(shout_dict, "delete") + await notify_shout(shout_dict, 'delete') return {} @@ -290,7 +273,7 @@ def handle_proposing(session, r, shout): if proposal.quote: proposal_diff = get_diff(shout.body, proposal.quote) proposal_dict = proposal.dict() - proposal_dict["quote"] = apply_diff( + proposal_dict['quote'] = apply_diff( replied_reaction.quote, proposal_diff ) Reaction.update(proposal, proposal_dict) @@ -298,7 +281,7 @@ def handle_proposing(session, r, shout): # patch shout's body shout_dict = shout.dict() - shout_dict["body"] = replied_reaction.quote + shout_dict['body'] = replied_reaction.quote Shout.update(shout, shout_dict) session.add(shout) session.commit() diff --git a/resolvers/follower.py b/resolvers/follower.py index 63653b78..2f66f7e2 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -26,16 +26,16 @@ from services.logger import root_logger as logger from services.rediscache import redis -@mutation.field("follow") +@mutation.field('follow') @login_required async def follow(_, info, what, slug): try: - user_id = info.context["user_id"] + user_id = info.context['user_id'] with local_session() as session: actor = session.query(Author).filter(Author.user == user_id).first() if actor: follower_id = actor.id - if what == "AUTHOR": + if what == 'AUTHOR': if author_follow(follower_id, slug): author = ( session.query(Author.id).where(Author.slug == slug).one() @@ -44,30 +44,30 @@ async def follow(_, info, what, slug): session.query(Author).where(Author.id == follower_id).one() ) await notify_follower(follower.dict(), author.id) - elif what == "TOPIC": + elif what == 'TOPIC': topic_follow(follower_id, slug) - elif what == "COMMUNITY": + elif what == 'COMMUNITY': community_follow(follower_id, slug) - elif what == "REACTIONS": + elif what == 'REACTIONS': reactions_follow(follower_id, slug) except Exception as e: logger.debug(info, what, slug) logger.error(e) - return {"error": str(e)} + return {'error': str(e)} return {} -@mutation.field("unfollow") +@mutation.field('unfollow') @login_required async def unfollow(_, info, what, slug): - user_id = info.context["user_id"] + user_id = info.context['user_id'] try: with local_session() as session: actor = session.query(Author).filter(Author.user == user_id).first() if actor: follower_id = actor.id - if what == "AUTHOR": + if what == 'AUTHOR': if author_unfollow(follower_id, slug): author = ( session.query(Author.id).where(Author.slug == slug).one() @@ -75,15 +75,15 @@ async def unfollow(_, info, what, slug): follower = ( session.query(Author).where(Author.id == follower_id).one() ) - await notify_follower(follower.dict(), author.id, "unfollow") - elif what == "TOPIC": + await notify_follower(follower.dict(), author.id, 'unfollow') + elif what == 'TOPIC': topic_unfollow(follower_id, slug) - elif what == "COMMUNITY": + elif what == 'COMMUNITY': community_unfollow(follower_id, slug) - elif what == "REACTIONS": + elif what == 'REACTIONS': reactions_unfollow(follower_id, slug) except Exception as e: - return {"error": str(e)} + return {'error': str(e)} return {} @@ -97,14 +97,14 @@ def query_follows(user_id: str): if isinstance(author, Author): author_id = author.id authors_query = ( - select(column("name"), column("id"), column("slug"), column("pic")) + select(column('name'), column('id'), column('slug'), column('pic')) .select_from(Author) .join(AuthorFollower, AuthorFollower.follower == author_id) .filter(AuthorFollower.author == Author.id) ) topics_query = ( - select(column("title"), column("id"), column("slug"), column("pic")) + select(column('title'), column('id'), column('slug'), column('pic')) .select_from(Topic) .join(TopicFollower, TopicFollower.follower == author_id) .filter(TopicFollower.topic == Topic.id) @@ -124,24 +124,24 @@ def query_follows(user_id: str): # communities = session.query(Community).all() return { - "topics": topics, - "authors": authors, + 'topics': topics, + 'authors': authors, # "shouts": shouts, - "communities": [{"id": 1, "name": "Дискурс", "slug": "discours"}], + 'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}], } async def get_follows_by_user_id(user_id: str): if user_id: - redis_key = f"user:{user_id}:follows" - res = await redis.execute("GET", redis_key) + redis_key = f'user:{user_id}:follows' + res = await redis.execute('GET', redis_key) if isinstance(res, str): follows = json.loads(res) return follows - logger.debug(f"getting follows for {user_id}") + logger.debug(f'getting follows for {user_id}') follows = query_follows(user_id) - await redis.execute("SET", redis_key, json.dumps(follows)) + await redis.execute('SET', redis_key, json.dumps(follows)) return follows @@ -227,7 +227,7 @@ def author_unfollow(follower_id, slug): return False -@query.field("get_topic_followers") +@query.field('get_topic_followers') async def get_topic_followers(_, _info, slug: str, topic_id: int) -> List[Author]: q = select(Author) q = add_topic_stat_columns(q) @@ -241,9 +241,9 @@ async def get_topic_followers(_, _info, slug: str, topic_id: int) -> List[Author return await get_topics_from_query(q) -@query.field("get_shout_followers") +@query.field('get_shout_followers') def get_shout_followers( - _, _info, slug: str = "", shout_id: int | None = None + _, _info, slug: str = '', shout_id: int | None = None ) -> List[Author]: followers = [] with local_session() as session: diff --git a/resolvers/reaction.py b/resolvers/reaction.py index eb686a3c..1167d053 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -21,22 +21,22 @@ from services.logger import root_logger as logger def add_stat_columns(q, aliased_reaction): q = q.outerjoin(aliased_reaction).add_columns( - func.sum(aliased_reaction.id).label("reacted_stat"), + func.sum(aliased_reaction.id).label('reacted_stat'), func.sum( case((aliased_reaction.kind == ReactionKind.COMMENT.value, 1), else_=0) - ).label("comments_stat"), + ).label('comments_stat'), func.sum( case((aliased_reaction.kind == ReactionKind.LIKE.value, 1), else_=0) - ).label("likes_stat"), + ).label('likes_stat'), func.sum( case((aliased_reaction.kind == ReactionKind.DISLIKE.value, 1), else_=0) - ).label("dislikes_stat"), + ).label('dislikes_stat'), func.max( case( (aliased_reaction.kind != ReactionKind.COMMENT.value, None), else_=aliased_reaction.created_at, ) - ).label("last_comment"), + ).label('last_comment'), ) return q @@ -101,7 +101,7 @@ def check_to_unfeature(session, rejecter_id, reaction): async def set_featured(session, shout_id): s = session.query(Shout).where(Shout.id == shout_id).first() s.featured_at = int(time.time()) - Shout.update(s, {"featured_at": int(time.time())}) + Shout.update(s, {'featured_at': int(time.time())}) author = session.query(Author).filter(Author.id == s.created_by).first() if author: await add_user_role(str(author.user)) @@ -111,7 +111,7 @@ async def set_featured(session, shout_id): def set_unfeatured(session, shout_id): s = session.query(Shout).where(Shout.id == shout_id).first() - Shout.update(s, {"featured_at": None}) + Shout.update(s, {'featured_at': None}) session.add(s) session.commit() @@ -124,7 +124,7 @@ async def _create_reaction(session, shout, author, reaction): # collaborative editing if ( - rdict.get("reply_to") + rdict.get('reply_to') and r.kind in RATING_REACTIONS and author.id in shout.authors ): @@ -137,42 +137,42 @@ async def _create_reaction(session, shout, author, reaction): await set_featured(session, shout.id) # reactions auto-following - reactions_follow(author.id, reaction["shout"], True) + reactions_follow(author.id, reaction['shout'], True) - rdict["shout"] = shout.dict() - rdict["created_by"] = author.dict() - rdict["stat"] = {"commented": 0, "reacted": 0, "rating": 0} + rdict['shout'] = shout.dict() + rdict['created_by'] = author.dict() + rdict['stat'] = {'commented': 0, 'reacted': 0, 'rating': 0} # notifications call - await notify_reaction(rdict, "create") + await notify_reaction(rdict, 'create') return rdict -@mutation.field("create_reaction") +@mutation.field('create_reaction') @login_required async def create_reaction(_, info, reaction): - user_id = info.context["user_id"] + user_id = info.context['user_id'] - shout_id = reaction.get("shout") + shout_id = reaction.get('shout') if not shout_id: - return {"error": "Shout ID is required to create a reaction."} + return {'error': 'Shout ID is required to create a reaction.'} try: with local_session() as session: shout = session.query(Shout).filter(Shout.id == shout_id).first() author = session.query(Author).filter(Author.user == user_id).first() if shout and author: - reaction["created_by"] = author.id - kind = reaction.get("kind") + reaction['created_by'] = author.id + kind = reaction.get('kind') shout_id = shout.id - if not kind and isinstance(reaction.get("body"), str): + if not kind and isinstance(reaction.get('body'), str): kind = ReactionKind.COMMENT.value if not kind: - return {"error": "cannot create reaction without a kind"} + return {'error': 'cannot create reaction without a kind'} if kind in RATING_REACTIONS: opposite_kind = ( @@ -188,7 +188,7 @@ async def create_reaction(_, info, reaction): Reaction.kind.in_(RATING_REACTIONS), ) ) - reply_to = reaction.get("reply_to") + reply_to = reaction.get('reply_to') if reply_to: q = q.filter(Reaction.reply_to == reply_to) rating_reactions = session.execute(q).all() @@ -201,31 +201,31 @@ async def create_reaction(_, info, reaction): rating_reactions, ) if same_rating: - return {"error": "You can't rate the same thing twice"} + return {'error': "You can't rate the same thing twice"} elif opposite_rating: - return {"error": "Remove opposite vote first"} + return {'error': 'Remove opposite vote first'} elif filter(lambda r: r.created_by == author.id, rating_reactions): - return {"error": "You can't rate your own thing"} + return {'error': "You can't rate your own thing"} rdict = await _create_reaction(session, shout, author, reaction) - return {"reaction": rdict} + return {'reaction': rdict} except Exception as e: import traceback traceback.print_exc() - logger.error(f"{type(e).__name__}: {e}") + logger.error(f'{type(e).__name__}: {e}') - return {"error": "Cannot create reaction."} + return {'error': 'Cannot create reaction.'} -@mutation.field("update_reaction") +@mutation.field('update_reaction') @login_required async def update_reaction(_, info, reaction): - user_id = info.context.get("user_id") - roles = info.context.get("roles") - rid = reaction.get("id") + user_id = info.context.get('user_id') + roles = info.context.get('roles') + rid = reaction.get('id') if rid and user_id and roles: - del reaction["id"] + del reaction['id'] with local_session() as session: reaction_query = select(Reaction).filter(Reaction.id == int(rid)) aliased_reaction = aliased(Reaction) @@ -238,19 +238,19 @@ async def update_reaction(_, info, reaction): ) if not r: - return {"error": "invalid reaction id"} + return {'error': 'invalid reaction id'} author = session.query(Author).filter(Author.user == user_id).first() if author: - if r.created_by != author.id and "editor" not in roles: - return {"error": "access denied"} + if r.created_by != author.id and 'editor' not in roles: + return {'error': 'access denied'} - body = reaction.get("body") + body = reaction.get('body') if body: r.body = body r.updated_at = int(time.time()) - if r.kind != reaction["kind"]: + if r.kind != reaction['kind']: # Определение изменения мнения может быть реализовано здесь pass @@ -259,79 +259,79 @@ async def update_reaction(_, info, reaction): session.commit() r.stat = { - "reacted": reacted_stat, - "commented": commented_stat, - "rating": int(likes_stat or 0) - int(dislikes_stat or 0), + 'reacted': reacted_stat, + 'commented': commented_stat, + 'rating': int(likes_stat or 0) - int(dislikes_stat or 0), } - await notify_reaction(r.dict(), "update") + await notify_reaction(r.dict(), 'update') - return {"reaction": r} + return {'reaction': r} else: - return {"error": "not authorized"} + return {'error': 'not authorized'} except Exception: import traceback traceback.print_exc() - return {"error": "cannot create reaction"} + return {'error': 'cannot create reaction'} -@mutation.field("delete_reaction") +@mutation.field('delete_reaction') @login_required async def delete_reaction(_, info, reaction_id: int): - user_id = info.context["user_id"] - roles = info.context["roles"] + user_id = info.context['user_id'] + roles = info.context['roles'] if isinstance(reaction_id, int) and user_id and isinstance(roles, list): with local_session() as session: try: author = session.query(Author).filter(Author.user == user_id).one() r = session.query(Reaction).filter(Reaction.id == reaction_id).one() if r and author: - if r.created_by is author.id and "editor" not in roles: - return {"error": "access denied"} + if r.created_by is author.id and 'editor' not in roles: + return {'error': 'access denied'} if r.kind in [ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]: session.delete(r) session.commit() - await notify_reaction(r.dict(), "delete") + await notify_reaction(r.dict(), 'delete') except Exception as exc: - return {"error": f"cannot delete reaction: {exc}"} - return {"error": "cannot delete reaction"} + return {'error': f'cannot delete reaction: {exc}'} + return {'error': 'cannot delete reaction'} def apply_reaction_filters(by, q): - shout_slug = by.get("shout", None) + shout_slug = by.get('shout', None) if shout_slug: q = q.filter(Shout.slug == shout_slug) - elif by.get("shouts"): - q = q.filter(Shout.slug.in_(by.get("shouts", []))) + elif by.get('shouts'): + q = q.filter(Shout.slug.in_(by.get('shouts', []))) - created_by = by.get("created_by", None) + created_by = by.get('created_by', None) if created_by: q = q.filter(Author.id == created_by) - topic = by.get("topic", None) + topic = by.get('topic', None) if topic: q = q.filter(Shout.topics.contains(topic)) - if by.get("comment", False): + if by.get('comment', False): q = q.filter(Reaction.kind == ReactionKind.COMMENT.value) - if by.get("rating", False): + if by.get('rating', False): q = q.filter(Reaction.kind.in_(RATING_REACTIONS)) - by_search = by.get("search", "") + by_search = by.get('search', '') if len(by_search) > 2: - q = q.filter(Reaction.body.ilike(f"%{by_search}%")) + q = q.filter(Reaction.body.ilike(f'%{by_search}%')) - after = by.get("after", None) + after = by.get('after', None) if isinstance(after, int): q = q.filter(Reaction.created_at > after) return q -@query.field("load_reactions_by") +@query.field('load_reactions_by') async def load_reactions_by(_, info, by, limit=50, offset=0): """ :param info: graphql meta @@ -368,7 +368,7 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): q = q.group_by(Reaction.id, Author.id, Shout.id, aliased_reaction.id) # order by - q = q.order_by(desc("created_at")) + q = q.order_by(desc('created_at')) # pagination q = q.limit(limit).offset(offset) @@ -389,19 +389,19 @@ async def load_reactions_by(_, info, by, limit=50, offset=0): reaction.created_by = author reaction.shout = shout reaction.stat = { - "rating": int(likes_stat or 0) - int(dislikes_stat or 0), - "reacted": reacted_stat, - "commented": commented_stat, + 'rating': int(likes_stat or 0) - int(dislikes_stat or 0), + 'reacted': reacted_stat, + 'commented': commented_stat, } reactions.add(reaction) # sort if by stat is present - stat_sort = by.get("stat") + stat_sort = by.get('stat') if stat_sort: reactions = sorted( reactions, key=lambda r: r.stat.get(stat_sort) or r.created_at, - reverse=stat_sort.startswith("-"), + reverse=stat_sort.startswith('-'), ) return reactions @@ -440,7 +440,7 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S # Sort shouts by the `last_comment` field combined_query = ( - union(q1, q2).order_by(desc("last_comment")).limit(limit).offset(offset) + union(q1, q2).order_by(desc('last_comment')).limit(limit).offset(offset) ) results = session.execute(combined_query).scalars() with local_session() as session: @@ -453,26 +453,26 @@ async def reacted_shouts_updates(follower_id: int, limit=50, offset=0) -> List[S 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, + '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 -@query.field("load_shouts_followed") +@query.field('load_shouts_followed') @login_required async def load_shouts_followed(_, info, limit=50, offset=0) -> List[Shout]: - user_id = info.context["user_id"] + user_id = info.context['user_id'] with local_session() as session: author = session.query(Author).filter(Author.user == user_id).first() if author: try: - author_id: int = author.dict()["id"] + author_id: int = author.dict()['id'] shouts = await reacted_shouts_updates(author_id, limit, offset) return shouts except Exception as error: diff --git a/resolvers/reader.py b/resolvers/reader.py index 19d707bc..985e37f0 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -18,22 +18,22 @@ from services.logger import root_logger as logger def apply_filters(q, filters, author_id=None): - if filters.get("reacted") and author_id: + if filters.get('reacted') and author_id: q.join(Reaction, Reaction.created_by == author_id) - by_featured = filters.get("featured") + by_featured = filters.get('featured') if by_featured: q = q.filter(Shout.featured_at.is_not(None)) - by_layouts = filters.get("layouts") + by_layouts = filters.get('layouts') if by_layouts: q = q.filter(Shout.layout.in_(by_layouts)) - by_author = filters.get("author") + by_author = filters.get('author') if by_author: q = q.filter(Shout.authors.any(slug=by_author)) - by_topic = filters.get("topic") + by_topic = filters.get('topic') if by_topic: q = q.filter(Shout.topics.any(slug=by_topic)) - by_after = filters.get("after") + by_after = filters.get('after') if by_after: ts = int(by_after) q = q.filter(Shout.created_at > ts) @@ -41,13 +41,10 @@ def apply_filters(q, filters, author_id=None): return q -@query.field("get_shout") +@query.field('get_shout') async def get_shout(_, _info, slug=None, shout_id=None): with local_session() as session: - q = select(Shout).options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + q = select(Shout).options(joinedload(Shout.authors), joinedload(Shout.topics)) aliased_reaction = aliased(Reaction) q = add_stat_columns(q, aliased_reaction) @@ -72,10 +69,10 @@ async def get_shout(_, _info, slug=None, shout_id=None): ] = results shout.stat = { - "viewed": await ViewedStorage.get_shout(shout.slug), - "reacted": reacted_stat, - "commented": commented_stat, - "rating": int(likes_stat or 0) - int(dislikes_stat or 0), + 'viewed': await ViewedStorage.get_shout(shout.slug), + 'reacted': reacted_stat, + 'commented': commented_stat, + 'rating': int(likes_stat or 0) - int(dislikes_stat or 0), } for author_caption in ( @@ -102,11 +99,11 @@ async def get_shout(_, _info, slug=None, shout_id=None): return shout except Exception: raise HTTPException( - status_code=404, detail=f"shout {slug or shout_id} not found" + status_code=404, detail=f'shout {slug or shout_id} not found' ) -@query.field("load_shouts_by") +@query.field('load_shouts_by') async def load_shouts_by(_, _info, options): """ :param options: { @@ -130,10 +127,7 @@ async def load_shouts_by(_, _info, options): # base q = ( select(Shout) - .options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) .where(and_(Shout.deleted_at.is_(None), Shout.layout.is_not(None))) ) @@ -142,7 +136,7 @@ async def load_shouts_by(_, _info, options): q = add_stat_columns(q, aliased_reaction) # filters - filters = options.get("filters", {}) + filters = options.get('filters', {}) q = apply_filters(q, filters) # group @@ -150,16 +144,16 @@ async def load_shouts_by(_, _info, options): # order order_by = options.get( - "order_by", Shout.featured_at if filters.get("featured") else Shout.published_at + 'order_by', Shout.featured_at if filters.get('featured') else Shout.published_at ) query_order_by = ( - desc(order_by) if options.get("order_by_desc", True) else asc(order_by) + desc(order_by) if options.get('order_by_desc', True) else asc(order_by) ) q = q.order_by(nulls_last(query_order_by)) # limit offset - offset = options.get("offset", 0) - limit = options.get("limit", 10) + offset = options.get('offset', 0) + limit = options.get('limit', 10) q = q.limit(limit).offset(offset) shouts = [] @@ -188,27 +182,24 @@ async def load_shouts_by(_, _info, options): if main_topic: shout.main_topic = main_topic[0] shout.stat = { - "viewed": await ViewedStorage.get_shout(shout.slug), - "reacted": reacted_stat, - "commented": commented_stat, - "rating": int(likes_stat) - int(dislikes_stat), + 'viewed': await ViewedStorage.get_shout(shout.slug), + 'reacted': reacted_stat, + 'commented': commented_stat, + 'rating': int(likes_stat) - int(dislikes_stat), } shouts.append(shout) return shouts -@query.field("load_shouts_drafts") +@query.field('load_shouts_drafts') @login_required async def load_shouts_drafts(_, info): - user_id = info.context["user_id"] + user_id = info.context['user_id'] q = ( select(Shout) - .options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) .filter(and_(Shout.deleted_at.is_(None), Shout.published_at.is_(None))) ) @@ -239,10 +230,10 @@ async def load_shouts_drafts(_, info): return shouts -@query.field("load_shouts_feed") +@query.field('load_shouts_feed') @login_required async def load_shouts_feed(_, info, options): - user_id = info.context["user_id"] + user_id = info.context['user_id'] shouts = [] with local_session() as session: @@ -267,10 +258,7 @@ async def load_shouts_feed(_, info, options): q = ( select(Shout) - .options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) .where( and_( Shout.published_at.is_not(None), @@ -282,19 +270,19 @@ async def load_shouts_feed(_, info, options): aliased_reaction = aliased(Reaction) q = add_stat_columns(q, aliased_reaction) - filters = options.get("filters", {}) + filters = options.get('filters', {}) q = apply_filters(q, filters, reader.id) order_by = options.get( - "order_by", - Shout.featured_at if filters.get("featured") else Shout.published_at, + 'order_by', + Shout.featured_at if filters.get('featured') else Shout.published_at, ) query_order_by = ( - desc(order_by) if options.get("order_by_desc", True) else asc(order_by) + desc(order_by) if options.get('order_by_desc', True) else asc(order_by) ) - offset = options.get("offset", 0) - limit = options.get("limit", 10) + offset = options.get('offset', 0) + limit = options.get('limit', 10) q = ( q.group_by(Shout.id) @@ -329,17 +317,17 @@ async def load_shouts_feed(_, info, options): if main_topic: shout.main_topic = main_topic[0] shout.stat = { - "viewed": await ViewedStorage.get_shout(shout.slug), - "reacted": reacted_stat, - "commented": commented_stat, - "rating": likes_stat - dislikes_stat, + 'viewed': await ViewedStorage.get_shout(shout.slug), + 'reacted': reacted_stat, + 'commented': commented_stat, + 'rating': likes_stat - dislikes_stat, } shouts.append(shout) return shouts -@query.field("load_shouts_search") +@query.field('load_shouts_search') async def load_shouts_search(_, _info, text, limit=50, offset=0): if isinstance(text, str) and len(text) > 2: results = await search_text(text, limit, offset) @@ -349,14 +337,11 @@ async def load_shouts_search(_, _info, text, limit=50, offset=0): @login_required -@query.field("load_shouts_unrated") +@query.field('load_shouts_unrated') async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0): q = ( select(Shout) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics), - ) + .options(selectinload(Shout.authors), selectinload(Shout.topics)) .outerjoin( Reaction, and_( @@ -367,7 +352,7 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0): ), ), ) - .outerjoin(Author, Author.user == bindparam("user_id")) + .outerjoin(Author, Author.user == bindparam('user_id')) .where( and_( Shout.deleted_at.is_(None), @@ -384,7 +369,7 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0): q = add_stat_columns(q, aliased_reaction) q = q.group_by(Shout.id).order_by(func.random()).limit(limit).offset(offset) - user_id = info.context.get("user_id") + user_id = info.context.get('user_id') if user_id: with local_session() as session: author = session.query(Author).filter(Author.user == user_id).first() @@ -404,20 +389,20 @@ async def get_shouts_from_query(q, author_id=None): likes_stat, dislikes_stat, last_comment, - ] in session.execute(q, {"author_id": author_id}).unique(): + ] in session.execute(q, {'author_id': author_id}).unique(): shouts.append(shout) shout.stat = { - "viewed": await ViewedStorage.get_shout(shout_slug=shout.slug), - "reacted": reacted_stat, - "commented": commented_stat, - "rating": int(likes_stat or 0) - int(dislikes_stat or 0), - "last_comment": last_comment, + 'viewed': await ViewedStorage.get_shout(shout_slug=shout.slug), + 'reacted': reacted_stat, + 'commented': commented_stat, + 'rating': int(likes_stat or 0) - int(dislikes_stat or 0), + 'last_comment': last_comment, } return shouts -@query.field("load_shouts_random_top") +@query.field('load_shouts_random_top') async def load_shouts_random_top(_, _info, options): """ :param _ @@ -440,7 +425,7 @@ async def load_shouts_random_top(_, _info, options): select(Shout.id).outerjoin(aliased_reaction).where(Shout.deleted_at.is_(None)) ) - subquery = apply_filters(subquery, options.get("filters", {})) + subquery = apply_filters(subquery, options.get('filters', {})) subquery = subquery.group_by(Shout.id).order_by( desc( func.sum( @@ -455,36 +440,33 @@ async def load_shouts_random_top(_, _info, options): ) ) - random_limit = options.get("random_limit") + random_limit = options.get('random_limit') if random_limit: subquery = subquery.limit(random_limit) q = ( select(Shout) - .options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) .where(Shout.id.in_(subquery)) ) aliased_reaction = aliased(Reaction) q = add_stat_columns(q, aliased_reaction) - limit = options.get("limit", 10) + limit = options.get('limit', 10) q = q.group_by(Shout.id).order_by(func.random()).limit(limit) return await get_shouts_from_query(q) -@query.field("load_shouts_random_topic") +@query.field('load_shouts_random_topic') async def load_shouts_random_topic(_, info, limit: int = 10): topic = get_random_topic() if topic: shouts = fetch_shouts_by_topic(topic, limit) if shouts: - return {"topic": topic, "shouts": shouts} + return {'topic': topic, 'shouts': shouts} return { - "error": "failed to get random topic after few retries", + 'error': 'failed to get random topic after few retries', shouts: [], topic: {}, } @@ -493,10 +475,7 @@ async def load_shouts_random_topic(_, info, limit: int = 10): def fetch_shouts_by_topic(topic, limit): q = ( select(Shout) - .options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) .filter( and_( Shout.deleted_at.is_(None), diff --git a/resolvers/topic.py b/resolvers/topic.py index 819f2ceb..269478d6 100644 --- a/resolvers/topic.py +++ b/resolvers/topic.py @@ -17,15 +17,15 @@ def add_topic_stat_columns(q): q = ( q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic) - .add_columns(func.count(distinct(ShoutTopic.shout)).label("shouts_stat")) + .add_columns(func.count(distinct(ShoutTopic.shout)).label('shouts_stat')) .outerjoin(aliased_shout_author, ShoutTopic.shout == aliased_shout_author.shout) .add_columns( - func.count(distinct(aliased_shout_author.author)).label("authors_stat") + func.count(distinct(aliased_shout_author.author)).label('authors_stat') ) .outerjoin(aliased_topic_follower) .add_columns( func.count(distinct(aliased_topic_follower.follower)).label( - "followers_stat" + 'followers_stat' ) ) ) @@ -40,17 +40,17 @@ async def get_topics_from_query(q): with local_session() as session: for [topic, shouts_stat, authors_stat, followers_stat] in session.execute(q): topic.stat = { - "shouts": shouts_stat, - "authors": authors_stat, - "followers": followers_stat, - "viewed": await ViewedStorage.get_topic(topic.slug), + 'shouts': shouts_stat, + 'authors': authors_stat, + 'followers': followers_stat, + 'viewed': await ViewedStorage.get_topic(topic.slug), } topics.append(topic) return topics -@query.field("get_topics_all") +@query.field('get_topics_all') async def get_topics_all(_, _info): q = select(Topic) q = add_topic_stat_columns(q) @@ -66,7 +66,7 @@ async def topics_followed_by(author_id): return await get_topics_from_query(q) -@query.field("get_topics_by_community") +@query.field('get_topics_by_community') async def get_topics_by_community(_, _info, community_id: int): q = select(Topic).where(Topic.community == community_id) q = add_topic_stat_columns(q) @@ -74,8 +74,8 @@ async def get_topics_by_community(_, _info, community_id: int): return await get_topics_from_query(q) -@query.field("get_topics_by_author") -async def get_topics_by_author(_, _info, author_id=None, slug="", user=""): +@query.field('get_topics_by_author') +async def get_topics_by_author(_, _info, author_id=None, slug='', user=''): q = select(Topic) q = add_topic_stat_columns(q) if author_id: @@ -88,7 +88,7 @@ async def get_topics_by_author(_, _info, author_id=None, slug="", user=""): return await get_topics_from_query(q) -@query.field("get_topic") +@query.field('get_topic') async def get_topic(_, _info, slug): q = select(Topic).where(Topic.slug == slug) q = add_topic_stat_columns(q) @@ -98,7 +98,7 @@ async def get_topic(_, _info, slug): return topics[0] -@mutation.field("create_topic") +@mutation.field('create_topic') @login_required async def create_topic(_, _info, inp): with local_session() as session: @@ -108,43 +108,43 @@ async def create_topic(_, _info, inp): session.add(new_topic) session.commit() - return {"topic": new_topic} + return {'topic': new_topic} -@mutation.field("update_topic") +@mutation.field('update_topic') @login_required async def update_topic(_, _info, inp): - slug = inp["slug"] + slug = inp['slug'] with local_session() as session: topic = session.query(Topic).filter(Topic.slug == slug).first() if not topic: - return {"error": "topic not found"} + return {'error': 'topic not found'} else: Topic.update(topic, inp) session.add(topic) session.commit() - return {"topic": topic} + return {'topic': topic} -@mutation.field("delete_topic") +@mutation.field('delete_topic') @login_required async def delete_topic(_, info, slug: str): - user_id = info.context["user_id"] + user_id = info.context['user_id'] with local_session() as session: t: Topic = session.query(Topic).filter(Topic.slug == slug).first() if not t: - return {"error": "invalid topic slug"} + return {'error': 'invalid topic slug'} author = session.query(Author).filter(Author.user == user_id).first() if author: if t.created_by != author.id: - return {"error": "access denied"} + return {'error': 'access denied'} session.delete(t) session.commit() return {} - return {"error": "access denied"} + return {'error': 'access denied'} def topic_follow(follower_id, slug): @@ -175,7 +175,7 @@ def topic_unfollow(follower_id, slug): return False -@query.field("get_topics_random") +@query.field('get_topics_random') async def get_topics_random(_, info, amount=12): q = select(Topic) q = q.join(ShoutTopic) diff --git a/server.py b/server.py index 956690d0..f35dc92e 100644 --- a/server.py +++ b/server.py @@ -2,12 +2,12 @@ from granian.constants import Interfaces from granian.server import Granian from services.logger import root_logger as logger -if __name__ == "__main__": - logger.info("started") +if __name__ == '__main__': + logger.info('started') granian_instance = Granian( - "main:app", - address="0.0.0.0", # noqa S104 + 'main:app', + address='0.0.0.0', # noqa S104 port=8000, threads=4, websockets=False, diff --git a/services/auth.py b/services/auth.py index abcd99c3..1a6139bd 100644 --- a/services/auth.py +++ b/services/auth.py @@ -9,31 +9,31 @@ from services.logger import root_logger as logger async def request_data(gql, headers=None): if headers is None: - headers = {"Content-Type": "application/json"} + headers = {'Content-Type': 'application/json'} try: async with httpx.AsyncClient() as client: response = await client.post(AUTH_URL, json=gql, headers=headers) if response.status_code == 200: data = response.json() - errors = data.get("errors") + errors = data.get('errors') if errors: - logger.error(f"HTTP Errors: {errors}") + logger.error(f'HTTP Errors: {errors}') else: return data except Exception as e: # Handling and logging exceptions during authentication check - logger.error(f"request_data error: {e}") + logger.error(f'request_data error: {e}') return None # Создание региона кэша с TTL 30 секунд -region = make_region().configure("dogpile.cache.memory", expiration_time=30) +region = make_region().configure('dogpile.cache.memory', expiration_time=30) # Функция-ключ для кэширования def auth_cache_key(req): - token = req.headers.get("Authorization") - return f"auth_token:{token}" + token = req.headers.get('Authorization') + return f'auth_token:{token}' # Декоратор для кэширования запроса проверки токена @@ -55,32 +55,27 @@ def cache_auth_request(f): # Измененная функция проверки аутентификации с кэшированием @cache_auth_request async def check_auth(req): - token = req.headers.get("Authorization") - user_id = "" + token = req.headers.get('Authorization') + user_id = '' user_roles = [] if token: try: # Logging the authentication token - logger.debug(f"{token}") - query_name = "validate_jwt_token" - operation = "ValidateToken" - variables = { - "params": { - "token_type": "access_token", - "token": token, - } - } + logger.debug(f'{token}') + query_name = 'validate_jwt_token' + operation = 'ValidateToken' + variables = {'params': {'token_type': 'access_token', 'token': token}} gql = { - "query": f"query {operation}($params: ValidateJWTTokenInput!) {{ {query_name}(params: $params) {{ is_valid claims }} }}", - "variables": variables, - "operationName": operation, + 'query': f'query {operation}($params: ValidateJWTTokenInput!) {{ {query_name}(params: $params) {{ is_valid claims }} }}', + 'variables': variables, + 'operationName': operation, } data = await request_data(gql) if data: - user_data = data.get("data", {}).get(query_name, {}).get("claims", {}) - user_id = user_data.get("sub") - user_roles = user_data.get("allowed_roles") + user_data = data.get('data', {}).get(query_name, {}).get('claims', {}) + user_id = user_data.get('sub') + user_roles = user_data.get('allowed_roles') except Exception as e: import traceback @@ -92,41 +87,41 @@ async def check_auth(req): async def add_user_role(user_id): - logger.info(f"add author role for user_id: {user_id}") - query_name = "_update_user" - operation = "UpdateUserRoles" + logger.info(f'add author role for user_id: {user_id}') + query_name = '_update_user' + operation = 'UpdateUserRoles' headers = { - "Content-Type": "application/json", - "x-authorizer-admin-secret": ADMIN_SECRET, + 'Content-Type': 'application/json', + 'x-authorizer-admin-secret': ADMIN_SECRET, } - variables = {"params": {"roles": "author, reader", "id": user_id}} + variables = {'params': {'roles': 'author, reader', 'id': user_id}} gql = { - "query": f"mutation {operation}($params: UpdateUserInput!) {{ {query_name}(params: $params) {{ id roles }} }}", - "variables": variables, - "operationName": operation, + 'query': f'mutation {operation}($params: UpdateUserInput!) {{ {query_name}(params: $params) {{ id roles }} }}', + 'variables': variables, + 'operationName': operation, } data = await request_data(gql, headers) if data: - user_id = data.get("data", {}).get(query_name, {}).get("id") + user_id = data.get('data', {}).get(query_name, {}).get('id') return user_id def login_required(f): @wraps(f) async def decorated_function(*args, **kwargs): - user_id = "" + user_id = '' user_roles = [] info = args[1] try: - req = info.context.get("request") + req = info.context.get('request') [user_id, user_roles] = await check_auth(req) except Exception as e: - logger.error(f"Failed to authenticate user: {e}") + logger.error(f'Failed to authenticate user: {e}') if user_id: - logger.info(f" got {user_id} roles: {user_roles}") - info.context["user_id"] = user_id.strip() - info.context["roles"] = user_roles + logger.info(f' got {user_id} roles: {user_roles}') + info.context['user_id'] = user_id.strip() + info.context['roles'] = user_roles return await f(*args, **kwargs) return decorated_function @@ -135,7 +130,7 @@ def login_required(f): def auth_request(f): @wraps(f) async def decorated_function(*args, **kwargs): - user_id = "" + user_id = '' user_roles = [] req = {} try: @@ -145,11 +140,11 @@ def auth_request(f): import traceback traceback.print_exc() - logger.error(f"Failed to authenticate user: {args} {e}") + logger.error(f'Failed to authenticate user: {args} {e}') if user_id: - logger.info(f" got {user_id} roles: {user_roles}") - req["user_id"] = user_id.strip() - req["roles"] = user_roles + logger.info(f' got {user_id} roles: {user_roles}') + req['user_id'] = user_id.strip() + req['roles'] = user_roles return await f(*args, **kwargs) return decorated_function diff --git a/services/db.py b/services/db.py index 58a8767b..c22d1bb3 100644 --- a/services/db.py +++ b/services/db.py @@ -14,11 +14,11 @@ from services.logger import root_logger as logger from settings import DB_URL # Создание региона кэша с TTL 300 секунд -cache_region = make_region().configure("dogpile.cache.memory", expiration_time=300) +cache_region = make_region().configure('dogpile.cache.memory', expiration_time=300) # Подключение к базе данных SQLAlchemy engine = create_engine(DB_URL, echo=False, pool_size=10, max_overflow=20) -T = TypeVar("T") +T = TypeVar('T') REGISTRY: Dict[str, type] = {} Base = declarative_base() @@ -29,9 +29,9 @@ def profile_sqlalchemy_queries(threshold=0.1): def wrapper(*args, **kw): elapsed, stat_loader, result = _profile(fn, threshold, *args, **kw) if elapsed is not None: - print(f"Query took {elapsed:.3f} seconds to execute.") + print(f'Query took {elapsed:.3f} seconds to execute.') stats = stat_loader() - stats.sort_stats("cumulative") + stats.sort_stats('cumulative') stats.print_stats() return result @@ -52,14 +52,14 @@ def _profile(fn, threshold, *args, **kw): # Перехватчики для журнала запросов SQLAlchemy -@event.listens_for(Engine, "before_cursor_execute") +@event.listens_for(Engine, 'before_cursor_execute') def before_cursor_execute(conn, cursor, statement, parameters, context, executemany): conn._query_start_time = time.time() -@event.listens_for(Engine, "after_cursor_execute") +@event.listens_for(Engine, 'after_cursor_execute') def after_cursor_execute(conn, cursor, statement, parameters, context, executemany): - if hasattr(conn, "_query_start_time"): + if hasattr(conn, '_query_start_time'): elapsed = time.time() - conn._query_start_time del conn._query_start_time if elapsed > 0.2: # Adjust threshold as needed @@ -71,7 +71,7 @@ def after_cursor_execute(conn, cursor, statement, parameters, context, executema profiler(statement, parameters) -def local_session(src=""): +def local_session(src=''): return Session(bind=engine, expire_on_commit=False) @@ -82,7 +82,7 @@ class Base(declarative_base()): __init__: Callable __allow_unmapped__ = True __abstract__ = True - __table_args__ = {"extend_existing": True} + __table_args__ = {'extend_existing': True} id = Column(Integer, primary_key=True) @@ -91,12 +91,12 @@ class Base(declarative_base()): def dict(self) -> Dict[str, Any]: column_names = self.__table__.columns.keys() - if "_sa_instance_state" in column_names: - column_names.remove("_sa_instance_state") + if '_sa_instance_state' in column_names: + column_names.remove('_sa_instance_state') try: return {c: getattr(self, c) for c in column_names} except Exception as e: - logger.error(f"Error occurred while converting object to dictionary: {e}") + logger.error(f'Error occurred while converting object to dictionary: {e}') return {} def update(self, values: Dict[str, Any]) -> None: diff --git a/services/diff.py b/services/diff.py index dacaf27b..75a99fac 100644 --- a/services/diff.py +++ b/services/diff.py @@ -29,19 +29,19 @@ def apply_diff(original, diff): The modified string. """ result = [] - pattern = re.compile(r"^(\+|-) ") + pattern = re.compile(r'^(\+|-) ') for line in diff: match = pattern.match(line) if match: op = match.group(1) content = line[2:] - if op == "+": + if op == '+': result.append(content) - elif op == "-": + elif op == '-': # Ignore deleted lines pass else: result.append(line) - return " ".join(result) + return ' '.join(result) diff --git a/services/follows.py b/services/follows.py index 105c4b96..4b126b26 100644 --- a/services/follows.py +++ b/services/follows.py @@ -12,48 +12,48 @@ from services.rediscache import redis from services.viewed import ViewedStorage -@event.listens_for(Author, "after_insert") -@event.listens_for(Author, "after_update") +@event.listens_for(Author, 'after_insert') +@event.listens_for(Author, 'after_update') def after_author_update(mapper, connection, author: Author): - redis_key = f"user:{author.user}:author" + redis_key = f'user:{author.user}:author' asyncio.create_task( redis.execute( - "set", + 'set', redis_key, json.dumps( { - "id": author.id, - "name": author.name, - "slug": author.slug, - "pic": author.pic, + 'id': author.id, + 'name': author.name, + 'slug': author.slug, + 'pic': author.pic, } ), ) ) -@event.listens_for(TopicFollower, "after_insert") +@event.listens_for(TopicFollower, 'after_insert') def after_topic_follower_insert(mapper, connection, target: TopicFollower): asyncio.create_task( handle_topic_follower_change(connection, target.topic, target.follower, True) ) -@event.listens_for(TopicFollower, "after_delete") +@event.listens_for(TopicFollower, 'after_delete') def after_topic_follower_delete(mapper, connection, target: TopicFollower): asyncio.create_task( handle_topic_follower_change(connection, target.topic, target.follower, False) ) -@event.listens_for(AuthorFollower, "after_insert") +@event.listens_for(AuthorFollower, 'after_insert') def after_author_follower_insert(mapper, connection, target: AuthorFollower): asyncio.create_task( handle_author_follower_change(connection, target.author, target.follower, True) ) -@event.listens_for(AuthorFollower, "after_delete") +@event.listens_for(AuthorFollower, 'after_delete') def after_author_follower_delete(mapper, connection, target: AuthorFollower): asyncio.create_task( handle_author_follower_change(connection, target.author, target.follower, False) @@ -63,26 +63,26 @@ def after_author_follower_delete(mapper, connection, target: AuthorFollower): async def update_follows_for_user( connection, user_id, entity_type, entity: dict, is_insert ): - redis_key = f"user:{user_id}:follows" + redis_key = f'user:{user_id}:follows' follows_str = await redis.get(redis_key) if follows_str: follows = json.loads(follows_str) else: follows = { - "topics": [], - "authors": [], - "communities": [ - {"slug": "discours", "name": "Дискурс", "id": 1, "desc": ""} + 'topics': [], + 'authors': [], + 'communities': [ + {'slug': 'discours', 'name': 'Дискурс', 'id': 1, 'desc': ''} ], } if is_insert: - follows[f"{entity_type}s"].append(entity) + follows[f'{entity_type}s'].append(entity) else: # Remove the entity from follows - follows[f"{entity_type}s"] = [ - e for e in follows[f"{entity_type}s"] if e["id"] != entity["id"] + follows[f'{entity_type}s'] = [ + e for e in follows[f'{entity_type}s'] if e['id'] != entity['id'] ] - await redis.execute("set", redis_key, json.dumps(follows)) + await redis.execute('set', redis_key, json.dumps(follows)) async def handle_author_follower_change(connection, author_id, follower_id, is_insert): @@ -93,17 +93,17 @@ async def handle_author_follower_change(connection, author_id, follower_id, is_i q ).first() author.stat = { - "shouts": shouts_stat, - "viewed": await ViewedStorage.get_author(author.slug), - "followers": followers_stat, - "followings": followings_stat, + 'shouts': shouts_stat, + 'viewed': await ViewedStorage.get_author(author.slug), + 'followers': followers_stat, + 'followings': followings_stat, } follower = await conn.execute( select(Author).filter(Author.id == follower_id) ).first() if follower and author: await update_follows_for_user( - connection, follower.user, "author", author.dict(), is_insert + connection, follower.user, 'author', author.dict(), is_insert ) @@ -115,17 +115,17 @@ async def handle_topic_follower_change(connection, topic_id, follower_id, is_ins q ).first() topic.stat = { - "shouts": shouts_stat, - "authors": authors_stat, - "followers": followers_stat, - "viewed": await ViewedStorage.get_topic(topic.slug), + 'shouts': shouts_stat, + 'authors': authors_stat, + 'followers': followers_stat, + 'viewed': await ViewedStorage.get_topic(topic.slug), } follower = connection.execute( select(Author).filter(Author.id == follower_id) ).first() if follower and topic: await update_follows_for_user( - connection, follower.user, "topic", topic.dict(), is_insert + connection, follower.user, 'topic', topic.dict(), is_insert ) @@ -149,24 +149,24 @@ class FollowsCached: @staticmethod async def update_author_cache(author: Author): - redis_key = f"user:{author.user}:author" + redis_key = f'user:{author.user}:author' if isinstance(author, Author): await redis.execute( - "set", + 'set', redis_key, json.dumps( { - "id": author.id, - "name": author.name, - "slug": author.slug, - "pic": author.pic, + 'id': author.id, + 'name': author.name, + 'slug': author.slug, + 'pic': author.pic, } ), ) follows = await get_author_follows(None, None, user=author.user) if isinstance(follows, dict): - redis_key = f"user:{author.user}:follows" - await redis.execute("set", redis_key, json.dumps(follows)) + redis_key = f'user:{author.user}:follows' + await redis.execute('set', redis_key, json.dumps(follows)) @staticmethod async def worker(): @@ -178,7 +178,7 @@ class FollowsCached: await asyncio.sleep(10 * 60 * 60) except asyncio.CancelledError: # Handle cancellation due to SIGTERM - logger.info("Cancellation requested. Cleaning up...") + logger.info('Cancellation requested. Cleaning up...') # Perform any necessary cleanup before exiting the loop break except Exception as exc: diff --git a/services/logger.py b/services/logger.py index d625c789..2edf17ee 100644 --- a/services/logger.py +++ b/services/logger.py @@ -3,45 +3,45 @@ import colorlog # Define the color scheme color_scheme = { - "DEBUG": "light_black", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red,bg_white", + 'DEBUG': 'light_black', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red,bg_white', } # Define secondary log colors secondary_colors = { - "log_name": {"DEBUG": "blue"}, - "asctime": {"DEBUG": "cyan"}, - "process": {"DEBUG": "purple"}, - "module": {"DEBUG": "light_black,bg_blue"}, + 'log_name': {'DEBUG': 'blue'}, + 'asctime': {'DEBUG': 'cyan'}, + 'process': {'DEBUG': 'purple'}, + 'module': {'DEBUG': 'light_black,bg_blue'}, } # Define the log format string -fmt_string = "%(log_color)s%(levelname)s: %(log_color)s[%(module)s]%(reset)s %(white)s%(message)s" +fmt_string = '%(log_color)s%(levelname)s: %(log_color)s[%(module)s]%(reset)s %(white)s%(message)s' # Define formatting configuration fmt_config = { - "log_colors": color_scheme, - "secondary_log_colors": secondary_colors, - "style": "%", - "reset": True, + 'log_colors': color_scheme, + 'secondary_log_colors': secondary_colors, + 'style': '%', + 'reset': True, } class MultilineColoredFormatter(colorlog.ColoredFormatter): def format(self, record): # Check if the message is multiline - if record.getMessage() and "\n" in record.getMessage(): + if record.getMessage() and '\n' in record.getMessage(): # Split the message into lines - lines = record.getMessage().split("\n") + lines = record.getMessage().split('\n') formatted_lines = [] for line in lines: # Format each line with the provided format formatted_lines.append(super().format(record)) # Join the formatted lines - return "\n".join(formatted_lines) + return '\n'.join(formatted_lines) else: # If not multiline or no message, use the default formatting return super().format(record) @@ -55,7 +55,7 @@ stream = logging.StreamHandler() stream.setFormatter(formatter) -def get_colorful_logger(name="main"): +def get_colorful_logger(name='main'): # Create and configure the logger logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) diff --git a/services/notify.py b/services/notify.py index bb34d57c..588c1eee 100644 --- a/services/notify.py +++ b/services/notify.py @@ -3,43 +3,43 @@ import json from services.rediscache import redis -async def notify_reaction(reaction, action: str = "create"): - channel_name = "reaction" - data = {"payload": reaction, "action": action} +async def notify_reaction(reaction, action: str = 'create'): + channel_name = 'reaction' + data = {'payload': reaction, 'action': action} try: await redis.publish(channel_name, json.dumps(data)) except Exception as e: - print(f"[services.notify] Failed to publish to channel {channel_name}: {e}") + print(f'[services.notify] Failed to publish to channel {channel_name}: {e}') -async def notify_shout(shout, action: str = "update"): - channel_name = "shout" - data = {"payload": shout, "action": action} +async def notify_shout(shout, action: str = 'update'): + channel_name = 'shout' + data = {'payload': shout, 'action': action} try: await redis.publish(channel_name, json.dumps(data)) except Exception as e: - print(f"[services.notify] Failed to publish to channel {channel_name}: {e}") + print(f'[services.notify] Failed to publish to channel {channel_name}: {e}') -async def notify_follower(follower: dict, author_id: int, action: str = "follow"): - channel_name = f"follower:{author_id}" +async def notify_follower(follower: dict, author_id: int, action: str = 'follow'): + channel_name = f'follower:{author_id}' try: # Simplify dictionary before publishing - simplified_follower = {k: follower[k] for k in ["id", "name", "slug", "pic"]} + simplified_follower = {k: follower[k] for k in ['id', 'name', 'slug', 'pic']} - data = {"payload": simplified_follower, "action": action} + data = {'payload': simplified_follower, 'action': action} # Convert data to JSON string json_data = json.dumps(data) # Ensure the data is not empty before publishing if not json_data: - raise ValueError("Empty data to publish.") + raise ValueError('Empty data to publish.') # Use the 'await' keyword when publishing await redis.publish(channel_name, json_data) except Exception as e: # Log the error and re-raise it - print(f"[services.notify] Failed to publish to channel {channel_name}: {e}") + print(f'[services.notify] Failed to publish to channel {channel_name}: {e}') raise diff --git a/services/rediscache.py b/services/rediscache.py index 0ca86db5..8382f9d0 100644 --- a/services/rediscache.py +++ b/services/rediscache.py @@ -20,11 +20,11 @@ class RedisCache: async def execute(self, command, *args, **kwargs): if self._client: try: - logger.debug(f"{command} {args} {kwargs}") + logger.debug(f'{command} {args} {kwargs}') for arg in args: if isinstance(arg, dict): - if arg.get("_sa_instance_state"): - del arg["_sa_instance_state"] + if arg.get('_sa_instance_state'): + del arg['_sa_instance_state'] r = await self._client.execute_command(command, *args, **kwargs) logger.debug(type(r)) logger.debug(r) @@ -55,4 +55,4 @@ class RedisCache: redis = RedisCache() -__all__ = ["redis"] +__all__ = ['redis'] diff --git a/services/search.py b/services/search.py index 6d8aed14..fa4fdc10 100644 --- a/services/search.py +++ b/services/search.py @@ -7,69 +7,60 @@ from opensearchpy import OpenSearch from services.logger import root_logger as logger from services.rediscache import redis -ELASTIC_HOST = os.environ.get("ELASTIC_HOST", "").replace("https://", "") -ELASTIC_USER = os.environ.get("ELASTIC_USER", "") -ELASTIC_PASSWORD = os.environ.get("ELASTIC_PASSWORD", "") -ELASTIC_PORT = os.environ.get("ELASTIC_PORT", 9200) -ELASTIC_AUTH = f"{ELASTIC_USER}:{ELASTIC_PASSWORD}" if ELASTIC_USER else "" +ELASTIC_HOST = os.environ.get('ELASTIC_HOST', '').replace('https://', '') +ELASTIC_USER = os.environ.get('ELASTIC_USER', '') +ELASTIC_PASSWORD = os.environ.get('ELASTIC_PASSWORD', '') +ELASTIC_PORT = os.environ.get('ELASTIC_PORT', 9200) +ELASTIC_AUTH = f'{ELASTIC_USER}:{ELASTIC_PASSWORD}' if ELASTIC_USER else '' ELASTIC_URL = os.environ.get( - "ELASTIC_URL", f"https://{ELASTIC_AUTH}@{ELASTIC_HOST}:{ELASTIC_PORT}" + 'ELASTIC_URL', f'https://{ELASTIC_AUTH}@{ELASTIC_HOST}:{ELASTIC_PORT}' ) REDIS_TTL = 86400 # 1 day in seconds index_settings = { - "settings": { - "index": { - "number_of_shards": 1, - "auto_expand_replicas": "0-all", - }, - "analysis": { - "analyzer": { - "ru": { - "tokenizer": "standard", - "filter": ["lowercase", "ru_stop", "ru_stemmer"], + 'settings': { + 'index': {'number_of_shards': 1, 'auto_expand_replicas': '0-all'}, + 'analysis': { + 'analyzer': { + 'ru': { + 'tokenizer': 'standard', + 'filter': ['lowercase', 'ru_stop', 'ru_stemmer'], } }, - "filter": { - "ru_stemmer": { - "type": "stemmer", - "language": "russian", - }, - "ru_stop": { - "type": "stop", - "stopwords": "_russian_", - }, + 'filter': { + 'ru_stemmer': {'type': 'stemmer', 'language': 'russian'}, + 'ru_stop': {'type': 'stop', 'stopwords': '_russian_'}, }, }, }, - "mappings": { - "properties": { - "body": {"type": "text", "analyzer": "ru"}, - "title": {"type": "text", "analyzer": "ru"}, + 'mappings': { + 'properties': { + 'body': {'type': 'text', 'analyzer': 'ru'}, + 'title': {'type': 'text', 'analyzer': 'ru'}, # 'author': {'type': 'text'}, } }, } -expected_mapping = index_settings["mappings"] +expected_mapping = index_settings['mappings'] class SearchService: - def __init__(self, index_name="search_index"): + def __init__(self, index_name='search_index'): self.index_name = index_name self.manager = Manager() self.client = None # Используем менеджер для создания Lock и Value self.lock = self.manager.Lock() - self.initialized_flag = self.manager.Value("i", 0) + self.initialized_flag = self.manager.Value('i', 0) # Only initialize the instance if it's not already initialized if not self.initialized_flag.value and ELASTIC_HOST: try: self.client = OpenSearch( - hosts=[{"host": ELASTIC_HOST, "port": ELASTIC_PORT}], + hosts=[{'host': ELASTIC_HOST, 'port': ELASTIC_PORT}], http_compress=True, http_auth=(ELASTIC_USER, ELASTIC_PASSWORD), use_ssl=True, @@ -78,34 +69,34 @@ class SearchService: ssl_show_warn=False, # ca_certs = ca_certs_path ) - logger.info(" Клиент OpenSearch.org подключен") + logger.info(' Клиент OpenSearch.org подключен') if self.lock.acquire(blocking=False): try: self.check_index() finally: self.lock.release() else: - logger.debug(" проверка пропущена") + logger.debug(' проверка пропущена') except Exception as exc: - logger.error(f" {exc}") + logger.error(f' {exc}') self.client = None def info(self): if isinstance(self.client, OpenSearch): - logger.info(" Поиск подключен") # : {self.client.info()}') + logger.info(' Поиск подключен') # : {self.client.info()}') else: - logger.info(" * Задайте переменные среды для подключения к серверу поиска") + logger.info(' * Задайте переменные среды для подключения к серверу поиска') def delete_index(self): if self.client: - logger.debug(f" Удаляем индекс {self.index_name}") + logger.debug(f' Удаляем индекс {self.index_name}') self.client.indices.delete(index=self.index_name, ignore_unavailable=True) def create_index(self): if self.client: if self.lock.acquire(blocking=False): try: - logger.debug(f" Создаём новый индекс: {self.index_name} ") + logger.debug(f' Создаём новый индекс: {self.index_name} ') self.client.indices.create( index=self.index_name, body=index_settings ) @@ -114,11 +105,11 @@ class SearchService: finally: self.lock.release() else: - logger.debug(" ..") + logger.debug(' ..') def put_mapping(self): if self.client: - logger.debug(f" Разметка индекации {self.index_name}") + logger.debug(f' Разметка индекации {self.index_name}') self.client.indices.put_mapping( index=self.index_name, body=expected_mapping ) @@ -142,36 +133,28 @@ class SearchService: finally: self.lock.release() else: - logger.debug(" ..") + logger.debug(' ..') def index(self, shout): if self.client: id_ = str(shout.id) - logger.debug(f" Индексируем пост {id_}") + logger.debug(f' Индексируем пост {id_}') self.client.index(index=self.index_name, id=id_, body=shout.dict()) async def search(self, text, limit, offset): - logger.debug(f" Ищем: {text}") - search_body = { - "query": {"match": {"_all": text}}, - } + logger.debug(f' Ищем: {text}') + search_body = {'query': {'match': {'_all': text}}} if self.client: search_response = self.client.search( index=self.index_name, body=search_body, size=limit, from_=offset ) - hits = search_response["hits"]["hits"] + hits = search_response['hits']['hits'] - results = [ - { - **hit["_source"], - "score": hit["_score"], - } - for hit in hits - ] + results = [{**hit['_source'], 'score': hit['_score']} for hit in hits] # Use Redis as cache with TTL - redis_key = f"search:{text}" - await redis.execute("SETEX", redis_key, REDIS_TTL, json.dumps(results)) + redis_key = f'search:{text}' + await redis.execute('SETEX', redis_key, REDIS_TTL, json.dumps(results)) return [] diff --git a/services/sentry.py b/services/sentry.py index 5e7f89ca..52b7568b 100644 --- a/services/sentry.py +++ b/services/sentry.py @@ -26,5 +26,5 @@ def start_sentry(): ], ) except Exception as e: - print("[services.sentry] init error") + print('[services.sentry] init error') print(e) diff --git a/services/unread.py b/services/unread.py index 29c3e1f8..e8b2a898 100644 --- a/services/unread.py +++ b/services/unread.py @@ -4,7 +4,7 @@ from services.rediscache import redis async def get_unread_counter(chat_id: str, author_id: int) -> int: - r = await redis.execute("LLEN", f"chats/{chat_id}/unread/{author_id}") + r = await redis.execute('LLEN', f'chats/{chat_id}/unread/{author_id}') if isinstance(r, str): return int(r) elif isinstance(r, int): @@ -14,7 +14,7 @@ async def get_unread_counter(chat_id: str, author_id: int) -> int: async def get_total_unread_counter(author_id: int) -> int: - chats_set = await redis.execute("SMEMBERS", f"chats_by_author/{author_id}") + chats_set = await redis.execute('SMEMBERS', f'chats_by_author/{author_id}') s = 0 if isinstance(chats_set, str): chats_set = json.loads(chats_set) diff --git a/services/viewed.py b/services/viewed.py index 5f412b6e..19f2e43f 100644 --- a/services/viewed.py +++ b/services/viewed.py @@ -20,9 +20,9 @@ from orm.topic import Topic from services.db import local_session from services.logger import root_logger as logger -GOOGLE_KEYFILE_PATH = os.environ.get("GOOGLE_KEYFILE_PATH", "/dump/google-service.json") -GOOGLE_PROPERTY_ID = os.environ.get("GOOGLE_PROPERTY_ID", "") -VIEWS_FILEPATH = "/dump/views.json" +GOOGLE_KEYFILE_PATH = os.environ.get('GOOGLE_KEYFILE_PATH', '/dump/google-service.json') +GOOGLE_PROPERTY_ID = os.environ.get('GOOGLE_PROPERTY_ID', '') +VIEWS_FILEPATH = '/dump/views.json' class ViewedStorage: @@ -42,12 +42,12 @@ class ViewedStorage: """Подключение к клиенту Google Analytics с использованием аутентификации""" self = ViewedStorage async with self.lock: - os.environ.setdefault("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_KEYFILE_PATH) + os.environ.setdefault('GOOGLE_APPLICATION_CREDENTIALS', GOOGLE_KEYFILE_PATH) if GOOGLE_KEYFILE_PATH and os.path.isfile(GOOGLE_KEYFILE_PATH): # Using a default constructor instructs the client to use the credentials # specified in GOOGLE_APPLICATION_CREDENTIALS environment variable. self.analytics_client = BetaAnalyticsDataClient() - logger.info(" * Клиент Google Analytics успешно авторизован") + logger.info(' * Клиент Google Analytics успешно авторизован') # Загрузка предварительно подсчитанных просмотров из файла JSON self.load_precounted_views() @@ -55,19 +55,19 @@ class ViewedStorage: if os.path.exists(VIEWS_FILEPATH): file_timestamp = os.path.getctime(VIEWS_FILEPATH) self.start_date = datetime.fromtimestamp(file_timestamp).strftime( - "%Y-%m-%d" + '%Y-%m-%d' ) - now_date = datetime.now().strftime("%Y-%m-%d") + now_date = datetime.now().strftime('%Y-%m-%d') if now_date == self.start_date: - logger.info(" * Данные актуализованы!") + logger.info(' * Данные актуализованы!') else: - logger.info(f" * Миграция проводилась: {self.start_date}") + logger.info(f' * Миграция проводилась: {self.start_date}') # Запуск фоновой задачи asyncio.create_task(self.worker()) else: - logger.info(" * Пожалуйста, добавьте ключевой файл Google Analytics") + logger.info(' * Пожалуйста, добавьте ключевой файл Google Analytics') self.disabled = True @staticmethod @@ -75,31 +75,31 @@ class ViewedStorage: """Загрузка предварительно подсчитанных просмотров из файла JSON""" self = ViewedStorage try: - with open(VIEWS_FILEPATH, "r") as file: + with open(VIEWS_FILEPATH, 'r') as file: precounted_views = json.load(file) self.views_by_shout.update(precounted_views) logger.info( - f" * {len(precounted_views)} публикаций с просмотрами успешно загружены." + f' * {len(precounted_views)} публикаций с просмотрами успешно загружены.' ) except Exception as e: - logger.error(f"Ошибка загрузки предварительно подсчитанных просмотров: {e}") + logger.error(f'Ошибка загрузки предварительно подсчитанных просмотров: {e}') @staticmethod async def update_pages(): """Запрос всех страниц от Google Analytics, отсортированных по количеству просмотров""" self = ViewedStorage - logger.info(" ⎧ Обновление данных просмотров от Google Analytics ---") + logger.info(' ⎧ Обновление данных просмотров от Google Analytics ---') if not self.disabled: try: start = time.time() async with self.lock: if self.analytics_client: request = RunReportRequest( - property=f"properties/{GOOGLE_PROPERTY_ID}", - dimensions=[Dimension(name="pagePath")], - metrics=[Metric(name="screenPageViews")], + property=f'properties/{GOOGLE_PROPERTY_ID}', + dimensions=[Dimension(name='pagePath')], + metrics=[Metric(name='screenPageViews')], date_ranges=[ - DateRange(start_date=self.start_date, end_date="today") + DateRange(start_date=self.start_date, end_date='today') ], ) response = self.analytics_client.run_report(request) @@ -113,7 +113,7 @@ class ViewedStorage: # Извлечение путей страниц из ответа Google Analytics if isinstance(row.dimension_values, list): page_path = row.dimension_values[0].value - slug = page_path.split("discours.io/")[-1] + slug = page_path.split('discours.io/')[-1] views_count = int(row.metric_values[0].value) # Обновление данных в хранилище @@ -126,10 +126,10 @@ class ViewedStorage: # Запись путей страниц для логирования slugs.add(slug) - logger.info(f" ⎪ Собрано страниц: {len(slugs)} ") + logger.info(f' ⎪ Собрано страниц: {len(slugs)} ') end = time.time() - logger.info(" ⎪ Обновление страниц заняло %fs " % (end - start)) + logger.info(' ⎪ Обновление страниц заняло %fs ' % (end - start)) except Exception as error: logger.error(error) @@ -209,18 +209,18 @@ class ViewedStorage: failed = 0 except Exception as _exc: failed += 1 - logger.info(" - Обновление не удалось #%d, ожидание 10 секунд" % failed) + logger.info(' - Обновление не удалось #%d, ожидание 10 секунд' % failed) if failed > 3: - logger.info(" - Больше не пытаемся обновить") + logger.info(' - Больше не пытаемся обновить') break if failed == 0: when = datetime.now(timezone.utc) + timedelta(seconds=self.period) t = format(when.astimezone().isoformat()) logger.info( - " ⎩ Следующее обновление: %s" - % (t.split("T")[0] + " " + t.split("T")[1].split(".")[0]) + ' ⎩ Следующее обновление: %s' + % (t.split('T')[0] + ' ' + t.split('T')[1].split('.')[0]) ) await asyncio.sleep(self.period) else: await asyncio.sleep(10) - logger.info(" - Попытка снова обновить данные") + logger.info(' - Попытка снова обновить данные') diff --git a/services/webhook.py b/services/webhook.py index 93924d13..0748c79e 100644 --- a/services/webhook.py +++ b/services/webhook.py @@ -15,13 +15,13 @@ class WebhookEndpoint(HTTPEndpoint): try: data = await request.json() if data: - auth = request.headers.get("Authorization") + auth = request.headers.get('Authorization') if auth: - if auth == os.environ.get("WEBHOOK_SECRET"): - user_id: str = data["user"]["id"] - name: str = data["user"]["given_name"] - slug: str = data["user"]["email"].split("@")[0] - slug: str = re.sub("[^0-9a-z]+", "-", slug.lower()) + if auth == os.environ.get('WEBHOOK_SECRET'): + user_id: str = data['user']['id'] + name: str = data['user']['given_name'] + slug: str = data['user']['email'].split('@')[0] + slug: str = re.sub('[^0-9a-z]+', '-', slug.lower()) with local_session() as session: author = ( session.query(Author) @@ -29,12 +29,12 @@ class WebhookEndpoint(HTTPEndpoint): .first() ) if author: - slug = slug + "-" + user_id.split("-").pop() + slug = slug + '-' + user_id.split('-').pop() await create_author(user_id, slug, name) - return JSONResponse({"status": "success"}) + return JSONResponse({'status': 'success'}) except Exception as e: import traceback traceback.print_exc() - return JSONResponse({"status": "error", "message": str(e)}, status_code=500) + return JSONResponse({'status': 'error', 'message': str(e)}, status_code=500) diff --git a/settings.py b/settings.py index 03204015..2fb628c1 100644 --- a/settings.py +++ b/settings.py @@ -3,15 +3,15 @@ from os import environ PORT = 8080 DB_URL = ( - environ.get("DATABASE_URL", "").replace("postgres://", "postgresql://") - or environ.get("DB_URL", "").replace("postgres://", "postgresql://") - or "postgresql://postgres@localhost:5432/discoursio" + environ.get('DATABASE_URL', '').replace('postgres://', 'postgresql://') + or environ.get('DB_URL', '').replace('postgres://', 'postgresql://') + or 'postgresql://postgres@localhost:5432/discoursio' ) -REDIS_URL = environ.get("REDIS_URL") or "redis://127.0.0.1" -API_BASE = environ.get("API_BASE") or "" -AUTH_URL = environ.get("AUTH_URL") or "" -SENTRY_DSN = environ.get("SENTRY_DSN") -DEV_SERVER_PID_FILE_NAME = "dev-server.pid" -MODE = "development" if "dev" in sys.argv else "production" +REDIS_URL = environ.get('REDIS_URL') or 'redis://127.0.0.1' +API_BASE = environ.get('API_BASE') or '' +AUTH_URL = environ.get('AUTH_URL') or '' +SENTRY_DSN = environ.get('SENTRY_DSN') +DEV_SERVER_PID_FILE_NAME = 'dev-server.pid' +MODE = 'development' if 'dev' in sys.argv else 'production' -ADMIN_SECRET = environ.get("AUTH_SECRET") or "nothing" +ADMIN_SECRET = environ.get('AUTH_SECRET') or 'nothing'