viewed-service-fixes
All checks were successful
deploy / deploy (push) Successful in 1m23s

This commit is contained in:
Untone 2023-12-17 23:30:20 +03:00
parent 2c6b872acb
commit a6c5243c06
20 changed files with 110 additions and 109 deletions

View File

@ -1,20 +1,21 @@
import os import os
from importlib import import_module from importlib import import_module
from os.path import exists from os.path import exists
from ariadne import load_schema_from_path, make_executable_schema from ariadne import load_schema_from_path, make_executable_schema
from ariadne.asgi import GraphQL from ariadne.asgi import GraphQL
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from sentry_sdk.integrations.ariadne import AriadneIntegration from sentry_sdk.integrations.ariadne import AriadneIntegration
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration from sentry_sdk.integrations.starlette import StarletteIntegration
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.routing import Route from starlette.routing import Route
from resolvers.webhook import WebhookEndpoint from resolvers.webhook import WebhookEndpoint
from services.rediscache import redis from services.rediscache import redis
from services.schema import resolvers from services.schema import resolvers
from settings import DEV_SERVER_PID_FILE_NAME, SENTRY_DSN, MODE from settings import DEV_SERVER_PID_FILE_NAME, MODE, SENTRY_DSN
import_module("resolvers") import_module("resolvers")
schema = make_executable_schema(load_schema_from_path("schemas/core.graphql"), resolvers) # type: ignore schema = make_executable_schema(load_schema_from_path("schemas/core.graphql"), resolvers) # type: ignore

View File

@ -1,9 +1,11 @@
from sqlalchemy import Column, ForeignKey, Enum, String from enum import Enum as Enumeration
from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from services.db import Base
from orm.author import Author from orm.author import Author
from orm.shout import Shout from orm.shout import Shout
from enum import Enum as Enumeration from services.db import Base
class InviteStatus(Enumeration): class InviteStatus(Enumeration):

View File

@ -12,25 +12,24 @@ SQLAlchemy = "^2.0.22"
psycopg2-binary = "^2.9.9" psycopg2-binary = "^2.9.9"
redis = {extras = ["hiredis"], version = "^5.0.1"} redis = {extras = ["hiredis"], version = "^5.0.1"}
uvicorn = "^0.24" uvicorn = "^0.24"
sentry-sdk = "^1.38.0" sentry-sdk = "^1.39.1"
starlette = "^0.32.0.post1" starlette = "^0.34.0"
gql = "^3.4.1" gql = "^3.4.1"
ariadne = "^0.21" ariadne = "^0.21"
aiohttp = "^3.9.1" aiohttp = "^3.9.1"
requests = "^2.31.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
setuptools = "^69.0.2" setuptools = "^69.0.2"
pyright = "^1.1.341"
pytest = "^7.4.2"
black = { version = "^23.12.0", python = ">=3.12" }
ruff = { version = "^0.1.8", python = ">=3.12" }
isort = "^5.13.2"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poetry.dev-dependencies]
pytest = "^7.4.2"
black = { version = "^23.12.0", python = ">=3.12" }
ruff = { version = "^0.1.8", python = ">=3.12" }
[tool.black] [tool.black]
line-length = 120 line-length = 120
target-version = ['py312'] target-version = ['py312']

View File

@ -1,40 +1,32 @@
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.author import ( from resolvers.author import (
get_author, get_author,
get_author_followed,
get_author_followers,
get_author_id, get_author_id,
load_authors_all, load_authors_all,
get_author_followers,
get_author_followed,
load_authors_by, load_authors_by,
update_profile,
rate_author, rate_author,
update_profile,
) )
from resolvers.community import get_communities_all, get_community
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.follower import follow, get_my_followed, unfollow
from resolvers.reaction import ( from resolvers.reaction import (
create_reaction, create_reaction,
update_reaction,
delete_reaction, delete_reaction,
load_reactions_by, load_reactions_by,
load_shouts_followed, load_shouts_followed,
update_reaction,
) )
from resolvers.topic import (
get_topics_by_author,
get_topics_by_community,
get_topics_all,
get_topic,
)
from resolvers.follower import follow, unfollow, get_my_followed
from resolvers.reader import ( from resolvers.reader import (
get_shout, get_shout,
load_shouts_by, load_shouts_by,
load_shouts_feed, load_shouts_feed,
load_shouts_random_top,
load_shouts_search, load_shouts_search,
load_shouts_unrated, load_shouts_unrated,
load_shouts_random_top,
) )
from resolvers.community import get_community, get_communities_all from resolvers.topic import get_topic, get_topics_all, get_topics_by_author, get_topics_by_community
__all__ = [ __all__ = [
# author # author

View File

@ -1,20 +1,21 @@
import time import time
from typing import List from typing import List
from sqlalchemy import and_, func, distinct, select, literal, case
from sqlalchemy import and_, case, distinct, func, literal, select
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from orm.reaction import Reaction, ReactionKind from orm.author import Author, AuthorFollower, AuthorRating
from services.auth import login_required
from services.db import local_session
from services.unread import get_total_unread_counter
from services.schema import mutation, query
from orm.community import Community from orm.community import Community
from orm.reaction import Reaction, ReactionKind
from orm.shout import ShoutAuthor, ShoutTopic from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic from orm.topic import Topic
from orm.author import AuthorFollower, Author, AuthorRating
from resolvers.community import followed_communities from resolvers.community import followed_communities
from resolvers.topic import followed_topics
from resolvers.reaction import reacted_shouts_updates as followed_reactions from resolvers.reaction import reacted_shouts_updates as followed_reactions
from resolvers.topic import followed_topics
from services.auth import login_required
from services.db import local_session
from services.schema import mutation, query
from services.unread import get_total_unread_counter
def add_author_stat_columns(q): def add_author_stat_columns(q):

View File

@ -1,9 +1,9 @@
from orm.author import Author
from orm.invite import Invite, InviteStatus
from orm.shout import Shout
from services.auth import login_required from services.auth import login_required
from services.db import local_session from services.db import local_session
from services.schema import mutation from services.schema import mutation
from orm.invite import Invite, InviteStatus
from orm.author import Author
from orm.shout import Shout
@mutation.field("accept_invite") @mutation.field("accept_invite")

View File

@ -1,10 +1,11 @@
from services.db import local_session from sqlalchemy import and_, distinct, func, literal, select
from services.schema import query from sqlalchemy.orm import aliased
from orm.author import Author from orm.author import Author
from orm.community import Community, CommunityAuthor from orm.community import Community, CommunityAuthor
from orm.shout import ShoutCommunity from orm.shout import ShoutCommunity
from sqlalchemy import select, distinct, func, literal, and_ from services.db import local_session
from sqlalchemy.orm import aliased from services.schema import query
def add_community_stat_columns(q): def add_community_stat_columns(q):

View File

@ -1,15 +1,16 @@
import time # For Unix timestamps import time # For Unix timestamps
from sqlalchemy import and_, select from sqlalchemy import and_, select
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from orm.author import Author from orm.author import Author
from services.auth import login_required
from services.db import local_session
from services.schema import mutation, query
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
from orm.topic import Topic from orm.topic import Topic
from resolvers.reaction import reactions_follow, reactions_unfollow from resolvers.reaction import reactions_follow, reactions_unfollow
from services.auth import login_required
from services.db import local_session
from services.notify import notify_shout from services.notify import notify_shout
from services.schema import mutation, query
@query.field("get_shouts_drafts") @query.field("get_shouts_drafts")

View File

@ -2,18 +2,18 @@ from typing import List
from sqlalchemy import select from sqlalchemy import select
from orm.community import Community, CommunityAuthor from orm.author import Author, AuthorFollower
from orm.community import Community
from orm.reaction import Reaction from orm.reaction import Reaction
from orm.shout import Shout from orm.shout import Shout
from orm.topic import Topic, TopicFollower from orm.topic import Topic, TopicFollower
from services.auth import login_required
from resolvers.author import author_follow, author_unfollow from resolvers.author import author_follow, author_unfollow
from resolvers.community import community_follow, community_unfollow
from resolvers.reaction import reactions_follow, reactions_unfollow from resolvers.reaction import reactions_follow, reactions_unfollow
from resolvers.topic import topic_follow, topic_unfollow from resolvers.topic import topic_follow, topic_unfollow
from resolvers.community import community_follow, community_unfollow from services.auth import login_required
from services.following import FollowingManager, FollowingResult
from services.db import local_session from services.db import local_session
from orm.author import Author, AuthorFollower from services.following import FollowingManager, FollowingResult
from services.notify import notify_follower from services.notify import notify_follower
from services.schema import mutation, query from services.schema import mutation, query

View File

@ -1,15 +1,16 @@
import time import time
from typing import List from typing import List
from sqlalchemy import and_, asc, desc, select, text, func, case from sqlalchemy import and_, asc, case, desc, func, select, text
from sqlalchemy.orm import aliased, joinedload from sqlalchemy.orm import aliased, joinedload
from services.notify import notify_reaction
from services.auth import login_required from orm.author import Author
from services.db import local_session
from services.schema import mutation, query
from orm.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutReactionsFollower from orm.shout import Shout, ShoutReactionsFollower
from orm.author import Author from services.auth import login_required
from services.db import local_session
from services.notify import notify_reaction
from services.schema import mutation, query
def add_reaction_stat_columns(q): def add_reaction_stat_columns(q):

View File

@ -1,15 +1,15 @@
from sqlalchemy import distinct, bindparam, or_ from sqlalchemy import bindparam, distinct, or_
from sqlalchemy.orm import aliased, joinedload from sqlalchemy.orm import aliased, joinedload
from sqlalchemy.sql.expression import and_, asc, case, desc, func, nulls_last, select from sqlalchemy.sql.expression import and_, asc, case, desc, func, nulls_last, select
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from orm.author import Author, AuthorFollower
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
from orm.topic import Topic, TopicFollower
from services.auth import login_required from services.auth import login_required
from services.db import local_session from services.db import local_session
from services.schema import query from services.schema import query
from orm.topic import TopicFollower, Topic
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
from orm.author import AuthorFollower, Author
from services.search import SearchService from services.search import SearchService
from services.viewed import ViewedStorage from services.viewed import ViewedStorage
@ -161,7 +161,9 @@ async def load_shouts_by(_, _info, options):
session.query(Topic.slug) session.query(Topic.slug)
.join( .join(
ShoutTopic, ShoutTopic,
and_(ShoutTopic.topic == Topic.id, ShoutTopic.shout == shout.id, ShoutTopic.main == True), and_(
ShoutTopic.topic == Topic.id, ShoutTopic.shout == shout.id, ShoutTopic.main == True
), # noqa: E712
) )
.first() .first()
) )

View File

@ -1,12 +1,12 @@
from sqlalchemy import and_, select, distinct, func from sqlalchemy import and_, distinct, func, select
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from orm.author import Author
from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic, TopicFollower
from services.auth import login_required from services.auth import login_required
from services.db import local_session from services.db import local_session
from services.schema import mutation, query from services.schema import mutation, query
from orm.shout import ShoutTopic, ShoutAuthor
from orm.topic import Topic, TopicFollower
from orm.author import Author
async def followed_topics(follower_id): async def followed_topics(follower_id):

View File

@ -1,4 +1,5 @@
import sys import sys
import uvicorn import uvicorn
from uvicorn.main import logger from uvicorn.main import logger
@ -58,5 +59,5 @@ if __name__ == "__main__":
if "dev" in sys.argv: if "dev" in sys.argv:
import os import os
os.environ.set("MODE", "development") os.environ["MODE"] = "development"
uvicorn.run("main:app", host="0.0.0.0", port=PORT, proxy_headers=True, server_header=True) uvicorn.run("main:app", host="0.0.0.0", port=PORT, proxy_headers=True, server_header=True)

View File

@ -1,4 +1,5 @@
from functools import wraps from functools import wraps
import aiohttp import aiohttp
from aiohttp.web import HTTPUnauthorized from aiohttp.web import HTTPUnauthorized

View File

@ -1,4 +1,5 @@
import json import json
from services.rediscache import redis from services.rediscache import redis

View File

@ -1,4 +1,5 @@
import redis.asyncio as aredis import redis.asyncio as aredis
from settings import REDIS_URL from settings import REDIS_URL
@ -45,16 +46,6 @@ class RedisCache:
return return
await self._client.publish(channel, data) await self._client.publish(channel, data)
async def lrange(self, key, start, stop):
if self._client:
print(f"[redis] LRANGE {key} {start} {stop}")
return await self._client.lrange(key, start, stop)
async def mget(self, key, *keys):
if self._client:
print(f"[redis] MGET {key} {keys}")
return await self._client.mget(key, *keys)
redis = RedisCache() redis = RedisCache()

View File

@ -1,5 +1,4 @@
from ariadne import QueryType, MutationType # , ScalarType from ariadne import MutationType, QueryType # , ScalarType
# datetime_scalar = ScalarType("DateTime") # datetime_scalar = ScalarType("DateTime")
query = QueryType() query = QueryType()

View File

@ -1,8 +1,11 @@
import asyncio import asyncio
import json import json
from typing import List
import aiohttp import aiohttp
from services.rediscache import redis
from orm.shout import Shout from orm.shout import Shout
from services.rediscache import redis
class SearchService: class SearchService:
@ -16,11 +19,12 @@ class SearchService:
SearchService.cache = {} SearchService.cache = {}
@staticmethod @staticmethod
async def search(text, limit, offset) -> [Shout]: async def search(text, limit: int = 50, offset: int = 0) -> List[Shout]:
cached = await redis.execute("GET", text) cached = await redis.execute("GET", text)
if not cached: if not cached:
async with SearchService.lock: async with SearchService.lock:
# Use aiohttp to send a request to ElasticSearch # Use aiohttp to send a request to ElasticSearch
# TODO: add limit offset usage
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
search_url = f"https://search.discours.io/search?q={text}" search_url = f"https://search.discours.io/search?q={text}"
async with session.get(search_url) as response: async with session.get(search_url) as response:

View File

@ -2,13 +2,14 @@ from services.rediscache import redis
async def get_unread_counter(chat_id: str, author_id: int) -> int: async def get_unread_counter(chat_id: str, author_id: int) -> int:
unread = await redis.execute("LLEN", f"chats/{chat_id}/unread/{author_id}") unread: int = await redis.execute("LLEN", f"chats/{chat_id}/unread/{author_id}") or 0
return unread or 0 return unread
async def get_total_unread_counter(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}")
unread = 0 unread = 0
if chats_set:
for chat_id in list(chats_set): for chat_id in list(chats_set):
n = await get_unread_counter(chat_id, author_id) n = await get_unread_counter(chat_id, author_id)
unread += n unread += n

View File

@ -1,14 +1,14 @@
import asyncio import asyncio
import time import time
from datetime import timedelta, timezone, datetime from datetime import datetime, timedelta, timezone
from os import environ from os import environ
from gql import Client, gql from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport from gql.transport.aiohttp import AIOHTTPTransport
from services.db import local_session from orm.shout import Shout, ShoutTopic
from orm.topic import Topic from orm.topic import Topic
from orm.shout import ShoutTopic, Shout from services.db import local_session
load_facts = gql( load_facts = gql(
""" query getDomains { """ query getDomains {
@ -60,7 +60,7 @@ class ViewedStorage:
pages = None pages = None
domains = None domains = None
period = 60 * 60 # every hour period = 60 * 60 # every hour
client = None client: Client | None = None
auth_result = None auth_result = None
disabled = False disabled = False
@ -70,7 +70,7 @@ class ViewedStorage:
self = ViewedStorage self = ViewedStorage
async with self.lock: async with self.lock:
if token: if token:
self.client = create_client({"Authorization": "Bearer %s" % str(token)}, schema=schema_str) self.client = create_client({"Authorization": f"Bearer {token}"}, schema=schema_str)
print("[services.viewed] * authorized permanently by ackee.discours.io: %s" % token) print("[services.viewed] * authorized permanently by ackee.discours.io: %s" % token)
else: else:
print("[services.viewed] * please set ACKEE_TOKEN") print("[services.viewed] * please set ACKEE_TOKEN")
@ -83,22 +83,19 @@ class ViewedStorage:
start = time.time() start = time.time()
self = ViewedStorage self = ViewedStorage
try: try:
async with self.client as session: if self.client:
self.pages = await session.execute(load_pages) self.pages = self.client.execute(load_pages)
self.pages = self.pages["domains"][0]["statistics"]["pages"] self.pages = self.pages["domains"][0]["statistics"]["pages"]
shouts = {} shouts = {}
try:
for page in self.pages: for page in self.pages:
p = page["value"].split("?")[0] p = page["value"].split("?")[0]
slug = p.split("discours.io/")[-1] slug = p.split("discours.io/")[-1]
shouts[slug] = page["count"] shouts[slug] = page["count"]
for slug in shouts.keys(): for slug in shouts.keys():
await ViewedStorage.increment(slug, shouts[slug]) await ViewedStorage.increment(slug, shouts[slug])
except Exception:
pass
print("[services.viewed] ⎪ %d pages collected " % len(shouts.keys())) print("[services.viewed] ⎪ %d pages collected " % len(shouts.keys()))
except Exception as e: except Exception as e:
raise e raise Exception(e)
end = time.time() end = time.time()
print("[services.viewed] ⎪ update_pages took %fs " % (end - start)) print("[services.viewed] ⎪ update_pages took %fs " % (end - start))
@ -106,8 +103,14 @@ class ViewedStorage:
@staticmethod @staticmethod
async def get_facts(): async def get_facts():
self = ViewedStorage self = ViewedStorage
facts = []
try:
if self.client:
async with self.lock: async with self.lock:
return await self.client.execute(load_facts) facts = self.client.execute(load_facts)
except Exception as er:
print(f"[services.viewed] get_facts error: {er}")
return facts or []
@staticmethod @staticmethod
async def get_shout(shout_slug): async def get_shout(shout_slug):
@ -138,7 +141,7 @@ class ViewedStorage:
"""updates topics counters by shout slug""" """updates topics counters by shout slug"""
self = ViewedStorage self = ViewedStorage
with local_session() as session: with local_session() as session:
for [shout_topic, topic] in ( for [_shout_topic, topic] in (
session.query(ShoutTopic, Topic).join(Topic).join(Shout).where(Shout.slug == shout_slug).all() session.query(ShoutTopic, Topic).join(Topic).join(Shout).where(Shout.slug == shout_slug).all()
): ):
if not self.by_topics.get(topic.slug): if not self.by_topics.get(topic.slug):