0.2.14
Some checks failed
deploy / deploy (push) Failing after 2m1s

This commit is contained in:
Untone 2023-11-22 19:38:39 +03:00
parent e2082b48d3
commit db76ba3733
22 changed files with 271 additions and 314 deletions

View File

@ -1,3 +1,11 @@
[0.2.14]
- schema: some fixes from migrator
- services: db access simpler, no contextmanager
- services: removed Base.create() method
- services: rediscache updated
- resolvers: many minor fixes
- resolvers: get_reacted_shouts_updates as followedReactions query
[0.2.13] [0.2.13]
- services: db context manager - services: db context manager
- services: ViewedStorage fixes - services: ViewedStorage fixes

View File

@ -25,17 +25,33 @@ apt install redis nginx
Then run nginx, redis and API server Then run nginx, redis and API server
``` ```
redis-server redis-server
poetry env use 3.12
poetry install poetry install
python3 server.py dev poetry run python server.py dev
``` ```
## Services
# How to do an authorized request ### Auth
Put the header 'Authorization' with token from signIn query or registerUser mutation. Put the header 'Authorization' with token from signIn query or registerUser mutation.
# How to debug Ackee ### Viewed
Set ACKEE_TOKEN var Set ACKEE_TOKEN var to collect stats
# test ### Seacrh
ElasticSearch
### Notifications
Connected using Redis PubSub channels
### Inbox
To get unread counter raw redis query to Inbox's data is used
### Following Manager
Internal service with async access to storage

View File

@ -1,10 +0,0 @@
from services.db import Base, engine
from orm.shout import Shout
from orm.community import Community
def init_tables():
Base.metadata.create_all(engine)
Shout.init_table()
Community.init_table()
print("[orm] tables initialized")

View File

@ -1,7 +1,9 @@
import time import time
from sqlalchemy import JSON as JSONType from sqlalchemy import JSON as JSONType
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from services.db import Base from services.db import Base

View File

@ -1,5 +1,7 @@
import time import time
from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy import Column, ForeignKey, Integer, String
from services.db import Base from services.db import Base
@ -20,4 +22,4 @@ class Collection(Base):
pic = Column(String, nullable=True, comment="Picture") pic = Column(String, nullable=True, comment="Picture")
created_at = Column(Integer, default=lambda: int(time.time())) 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())) publishedAt = Column(Integer, default=lambda: int(time.time()))

View File

@ -1,9 +1,10 @@
import time import time
from sqlalchemy import Column, String, ForeignKey, Integer
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from services.db import Base, local_session
from orm.author import Author from orm.author import Author
from services.db import Base, local_session
class CommunityAuthor(Base): class CommunityAuthor(Base):
@ -29,10 +30,12 @@ class Community(Base):
@staticmethod @staticmethod
def init_table(): def init_table():
with local_session() as session: with local_session("orm.community") as session:
d = session.query(Community).filter(Community.slug == "discours").first() d = session.query(Community).filter(Community.slug == "discours").first()
if not d: if not d:
d = Community.create(name="Дискурс", slug="discours") d = Community(name="Дискурс", slug="discours")
print("[orm] created community %s" % d.slug) session.add(d)
session.commit()
print("[orm.community] created community %s" % d.slug)
Community.default_community = d Community.default_community = d
print("[orm] default community is %s" % d.slug) print("[orm.community] default community is %s" % d.slug)

View File

@ -1,7 +1,9 @@
from enum import Enum as Enumeration
from sqlalchemy import Column, Integer, Enum, ForeignKey, String
from services.db import Base
import time import time
from enum import Enum as Enumeration
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
from services.db import Base
class ReactionKind(Enumeration): class ReactionKind(Enumeration):
@ -33,5 +35,7 @@ class Reaction(Base):
deleted_by = Column(ForeignKey("author.id"), nullable=True, index=True) deleted_by = Column(ForeignKey("author.id"), nullable=True, index=True)
shout = Column(ForeignKey("shout.id"), nullable=False, index=True) shout = Column(ForeignKey("shout.id"), nullable=False, index=True)
reply_to = Column(ForeignKey("reaction.id"), nullable=True) reply_to = Column(ForeignKey("reaction.id"), nullable=True)
quote = Column(String, nullable=True, comment="a quoted fragment") range = Column(String, nullable=True, comment="<start index>:<end>")
kind = Column(Enum(ReactionKind), nullable=False) kind = Column(Enum(ReactionKind), nullable=False)
oid = Column(String)

View File

@ -1,20 +1,14 @@
import time import time
from enum import Enum as Enumeration from enum import Enum as Enumeration
from sqlalchemy import (
Enum, from sqlalchemy import JSON, Boolean, Column, Enum, ForeignKey, Integer, String
Boolean, from sqlalchemy.orm import relationship
Column,
ForeignKey, from orm.author import Author
Integer,
String,
JSON,
)
from sqlalchemy.orm import column_property, relationship
from services.db import Base, local_session
from orm.community import Community from orm.community import Community
from orm.reaction import Reaction from orm.reaction import Reaction
from orm.topic import Topic from orm.topic import Topic
from orm.author import Author from services.db import Base
class ShoutTopic(Base): class ShoutTopic(Base):
@ -67,7 +61,6 @@ class Shout(Base):
published_at = Column(Integer, nullable=True) published_at = Column(Integer, nullable=True)
deleted_at = Column(Integer, nullable=True) deleted_at = Column(Integer, nullable=True)
created_by = Column(ForeignKey("author.id"), comment="Created By")
deleted_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")
@ -80,9 +73,9 @@ class Shout(Base):
layout = Column(String, nullable=True) layout = Column(String, nullable=True)
media = Column(JSON, nullable=True) media = Column(JSON, nullable=True)
authors = relationship(lambda: Author, secondary=ShoutAuthor.__tablename__) authors = relationship(lambda: Author, secondary="shout_author")
topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__) topics = relationship(lambda: Topic, secondary="shout_topic")
communities = relationship(lambda: Community, secondary=ShoutCommunity.__tablename__) communities = relationship(lambda: Community, secondary="shout_community")
reactions = relationship(lambda: Reaction) reactions = relationship(lambda: Reaction)
visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS) visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS)
@ -91,15 +84,3 @@ class Shout(Base):
version_of = Column(ForeignKey("shout.id"), nullable=True) version_of = Column(ForeignKey("shout.id"), nullable=True)
oid = Column(String, nullable=True) oid = Column(String, nullable=True)
@staticmethod
def init_table():
with local_session() as session:
s = session.query(Shout).first()
if not s:
entry = {
"slug": "genesis-block",
"body": "",
"title": "Ничего",
"lang": "ru",
}
s = Shout.create(**entry)

View File

@ -1,5 +1,7 @@
import time import time
from sqlalchemy import Boolean, Column, Integer, ForeignKey, String
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from services.db import Base from services.db import Base

30
orm/user.py Normal file
View File

@ -0,0 +1,30 @@
import time
from sqlalchemy import JSON, Boolean, Column, Integer, String
from services.db import Base
class User(Base):
__tablename__ = "authorizer_users"
id = Column(String, primary_key=True, unique=True, nullable=False, default=None)
key = Column(String)
email = Column(String, unique=True)
email_verified_at = Column(Integer)
family_name = Column(String)
gender = Column(String)
given_name = Column(String)
is_multi_factor_auth_enabled = Column(Boolean)
middle_name = Column(String)
nickname = Column(String)
password = Column(String)
phone_number = Column(String, unique=True)
phone_number_verified_at = Column(Integer)
# preferred_username = Column(String, nullable=False)
picture = Column(String)
revoked_timestamp = Column(Integer)
roles = Column(JSON)
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()))

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "discoursio-core" name = "discoursio-core"
version = "0.2.13" version = "0.2.14"
description = "core module for discours.io" description = "core module for discours.io"
authors = ["discoursio devteam"] authors = ["discoursio devteam"]
license = "MIT" license = "MIT"
@ -11,7 +11,7 @@ python = "^3.12"
SQLAlchemy = "^2.0.22" SQLAlchemy = "^2.0.22"
httpx = "^0.25.0" httpx = "^0.25.0"
redis = {extras = ["hiredis"], version = "^5.0.1"} redis = {extras = ["hiredis"], version = "^5.0.1"}
uvicorn = "^0.23.2" uvicorn = "^0.24"
sentry-sdk = "^1.32.0" sentry-sdk = "^1.32.0"
gql = {git = "https://github.com/graphql-python/gql.git", rev = "master"} gql = {git = "https://github.com/graphql-python/gql.git", rev = "master"}
starlette = {git = "https://github.com/encode/starlette.git", rev = "master"} starlette = {git = "https://github.com/encode/starlette.git", rev = "master"}

View File

@ -1,24 +0,0 @@
git+https://github.com/tonyrewin/ariadne.git#master
git+https://github.com/encode/starlette.git#master
git+https://github.com/graphql-python/gql.git#master
SQLAlchemy
uvicorn
redis[hiredis]
itsdangerous
Authlib
PyJWT
PyYAML
httpx
psycopg2-binary
bcrypt
sentry-sdk
boto3
botocore
transliterate
passlib
pydantic
flake8
isort
brunette
mypy

View File

@ -12,7 +12,7 @@ from orm.topic import Topic
from orm.author import AuthorFollower, Author, AuthorRating from orm.author import AuthorFollower, Author, AuthorRating
from community import followed_communities from community import followed_communities
from topic import followed_topics from topic import followed_topics
from reaction import load_followed_reactions from reaction import reacted_shouts_updates as followed_reactions
def add_author_stat_columns(q): def add_author_stat_columns(q):
@ -28,16 +28,13 @@ def add_author_stat_columns(q):
func.count(distinct(followers_table.follower)).label("followers_stat") func.count(distinct(followers_table.follower)).label("followers_stat")
) )
q = q.outerjoin( q = q.outerjoin(followings_table, followings_table.follower == Author.id).add_columns(
followings_table, followings_table.follower == Author.id
).add_columns(
func.count(distinct(followings_table.author)).label("followings_stat") func.count(distinct(followings_table.author)).label("followings_stat")
) )
q = q.add_columns(literal(0).label("rating_stat")) q = q.add_columns(literal(0).label("rating_stat"))
# FIXME # FIXME
# q = q.outerjoin(author_rating_aliased, author_rating_aliased.user == Author.id).add_columns( # q = q.outerjoin(author_rating_aliased, author_rating_aliased.user == Author.id).add_columns(
# # TODO: check
# func.sum(author_rating_aliased.value).label('rating_stat') # func.sum(author_rating_aliased.value).label('rating_stat')
# ) # )
@ -83,16 +80,10 @@ def get_authors_from_query(q):
async def author_followings(author_id: int): async def author_followings(author_id: int):
return { return {
"unread": await get_total_unread_counter(author_id), # unread inbox messages counter "unread": await get_total_unread_counter(author_id), # unread inbox messages counter
"topics": [ "topics": [t.slug for t in await followed_topics(author_id)], # followed topics slugs
t.slug for t in await followed_topics(author_id) "authors": [a.slug for a in await followed_authors(author_id)], # followed authors slugs
], # followed topics slugs "reactions": [s.slug for s in await followed_reactions(author_id)], # fresh reacted shouts slugs
"authors": [ "communities": [c.slug for c in await followed_communities(author_id)], # communities
a.slug for a in await followed_authors(author_id)
], # followed authors slugs
"reactions": await load_followed_reactions(author_id),
"communities": [
c.slug for c in await followed_communities(author_id)
], # communities
} }
@ -102,7 +93,8 @@ async def update_profile(_, info, profile):
author_id = info.context["author_id"] author_id = info.context["author_id"]
with local_session() as session: with local_session() as session:
author = session.query(Author).where(Author.id == author_id).first() author = session.query(Author).where(Author.id == author_id).first()
author.update(profile) Author.update(author, profile)
session.add(author)
session.commit() session.commit()
return {"error": None, "author": author} return {"error": None, "author": author}
@ -112,7 +104,7 @@ def author_follow(follower_id, slug):
try: try:
with local_session() as session: with local_session() as session:
author = session.query(Author).where(Author.slug == slug).one() author = session.query(Author).where(Author.slug == slug).one()
af = AuthorFollower.create(follower=follower_id, author=author.id) af = AuthorFollower(follower=follower_id, author=author.id)
session.add(af) session.add(af)
session.commit() session.commit()
return True return True
@ -163,12 +155,7 @@ async def load_authors_by(_, _info, by, limit, offset):
elif by.get("name"): elif by.get("name"):
q = q.filter(Author.name.ilike(f"%{by['name']}%")) q = q.filter(Author.name.ilike(f"%{by['name']}%"))
elif by.get("topic"): elif by.get("topic"):
q = ( q = q.join(ShoutAuthor).join(ShoutTopic).join(Topic).where(Topic.slug == by["topic"])
q.join(ShoutAuthor)
.join(ShoutTopic)
.join(Topic)
.where(Topic.slug == by["topic"])
)
if by.get("last_seen"): # in unixtime if by.get("last_seen"): # in unixtime
before = int(time.time()) - by["last_seen"] before = int(time.time()) - by["last_seen"]
@ -211,9 +198,7 @@ async def author_followers(_, _info, slug) -> List[Author]:
async def followed_authors(follower_id): async def followed_authors(follower_id):
q = select(Author) q = select(Author)
q = add_author_stat_columns(q) q = add_author_stat_columns(q)
q = q.join(AuthorFollower, AuthorFollower.author == Author.id).where( q = q.join(AuthorFollower, AuthorFollower.author == Author.id).where(AuthorFollower.follower == follower_id)
AuthorFollower.follower == follower_id
)
# Pass the query to the get_authors_from_query function and return the results # Pass the query to the get_authors_from_query function and return the results
return get_authors_from_query(q) return get_authors_from_query(q)
@ -226,20 +211,19 @@ async def rate_author(_, info, rated_user_id, value):
with local_session() as session: with local_session() as session:
rating = ( rating = (
session.query(AuthorRating) session.query(AuthorRating)
.filter( .filter(and_(AuthorRating.rater == author_id, AuthorRating.user == rated_user_id))
and_(
AuthorRating.rater == author_id,
AuthorRating.user == rated_user_id
)
)
.first() .first()
) )
if rating: if rating:
rating.value = value rating.value = value
session.add(rating)
session.commit() session.commit()
return {} return {}
try: else:
AuthorRating.create(rater=author_id, user=rated_user_id, value=value) try:
except Exception as err: rating = AuthorRating(rater=author_id, user=rated_user_id, value=value)
return {"error": err} session.add(rating)
session.commit()
except Exception as err:
return {"error": err}
return {} return {}

View File

@ -14,9 +14,7 @@ def add_community_stat_columns(q):
q = q.outerjoin(shout_community_aliased).add_columns( 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( q = q.outerjoin(community_followers, community_followers.author == Author.id).add_columns(
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")
) )
@ -75,7 +73,7 @@ def community_follow(follower_id, slug):
try: try:
with local_session() as session: with local_session() as session:
community = session.query(Community).where(Community.slug == slug).one() community = session.query(Community).where(Community.slug == slug).one()
cf = CommunityAuthor.create(author=follower_id, community=community.id) cf = CommunityAuthor(author=follower_id, community=community.id)
session.add(cf) session.add(cf)
session.commit() session.commit()
return True return True

View File

@ -9,6 +9,7 @@ from orm.topic import Topic
from reaction import reactions_follow, reactions_unfollow from reaction import reactions_follow, reactions_unfollow
from services.notify import notify_shout from services.notify import notify_shout
@query.field("loadDrafts") @query.field("loadDrafts")
async def get_drafts(_, info): async def get_drafts(_, info):
author = info.context["request"].author author = info.context["request"].author
@ -27,17 +28,16 @@ async def get_drafts(_, info):
shouts.append(shout) shouts.append(shout)
return shouts return shouts
@mutation.field("createShout") @mutation.field("createShout")
@login_required @login_required
async def create_shout(_, info, inp): async def create_shout(_, info, inp):
author_id = info.context["author_id"] author_id = info.context["author_id"]
with local_session() as session: with local_session() as session:
topics = ( topics = session.query(Topic).filter(Topic.slug.in_(inp.get("topics", []))).all()
session.query(Topic).filter(Topic.slug.in_(inp.get("topics", []))).all()
)
# Replace datetime with Unix timestamp # Replace datetime with Unix timestamp
current_time = int(time.time()) current_time = int(time.time())
new_shout = Shout.create( new_shout = Shout(
**{ **{
"title": inp.get("title"), "title": inp.get("title"),
"subtitle": inp.get("subtitle"), "subtitle": inp.get("subtitle"),
@ -54,22 +54,23 @@ async def create_shout(_, info, inp):
} }
) )
for topic in topics: for topic in topics:
t = ShoutTopic.create(topic=topic.id, shout=new_shout.id) t = ShoutTopic(topic=topic.id, shout=new_shout.id)
session.add(t) session.add(t)
# NOTE: shout made by one first author # NOTE: shout made by one first author
sa = ShoutAuthor.create(shout=new_shout.id, author=author_id) sa = ShoutAuthor(shout=new_shout.id, author=author_id)
session.add(sa) session.add(sa)
session.add(new_shout) session.add(new_shout)
reactions_follow(author_id, new_shout.id, True) reactions_follow(author_id, new_shout.id, True)
session.commit() session.commit()
# TODO: GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug)
if new_shout.slug is None: if new_shout.slug is None:
new_shout.slug = f"draft-{new_shout.id}" new_shout.slug = f"draft-{new_shout.id}"
session.commit() session.commit()
else: else:
notify_shout(new_shout.dict(), "create") await notify_shout(new_shout.dict(), "create")
return {"shout": new_shout} return {"shout": new_shout}
@mutation.field("updateShout") @mutation.field("updateShout")
@login_required @login_required
async def update_shout(_, info, shout_id, shout_input=None, publish=False): async def update_shout(_, info, shout_id, shout_input=None, publish=False):
@ -93,42 +94,30 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
topics_input = shout_input["topics"] topics_input = shout_input["topics"]
del shout_input["topics"] del shout_input["topics"]
new_topics_to_link = [] new_topics_to_link = []
new_topics = [ new_topics = [topic_input for topic_input in topics_input if topic_input["id"] < 0]
topic_input for topic_input in topics_input if topic_input["id"] < 0
]
for new_topic in new_topics: for new_topic in new_topics:
del new_topic["id"] del new_topic["id"]
created_new_topic = Topic.create(**new_topic) created_new_topic = Topic(**new_topic)
session.add(created_new_topic) session.add(created_new_topic)
new_topics_to_link.append(created_new_topic) new_topics_to_link.append(created_new_topic)
if len(new_topics) > 0: if len(new_topics) > 0:
session.commit() session.commit()
for new_topic_to_link in new_topics_to_link: for new_topic_to_link in new_topics_to_link:
created_unlinked_topic = ShoutTopic.create( created_unlinked_topic = ShoutTopic(shout=shout.id, topic=new_topic_to_link.id)
shout=shout.id, topic=new_topic_to_link.id
)
session.add(created_unlinked_topic) session.add(created_unlinked_topic)
existing_topics_input = [ 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_to_link_ids = [
existing_topic_input["id"] existing_topic_input["id"]
for existing_topic_input in existing_topics_input for existing_topic_input in existing_topics_input
if existing_topic_input["id"] if existing_topic_input["id"] not in [topic.id for topic in shout.topics]
not in [topic.id for topic in shout.topics]
] ]
for existing_topic_to_link_id in existing_topic_to_link_ids: for existing_topic_to_link_id in existing_topic_to_link_ids:
created_unlinked_topic = ShoutTopic.create( created_unlinked_topic = ShoutTopic(shout=shout.id, topic=existing_topic_to_link_id)
shout=shout.id, topic=existing_topic_to_link_id
)
session.add(created_unlinked_topic) session.add(created_unlinked_topic)
topic_to_unlink_ids = [ topic_to_unlink_ids = [
topic.id topic.id
for topic in shout.topics for topic in shout.topics
if topic.id if topic.id not in [topic_input["id"] for topic_input in existing_topics_input]
not in [topic_input["id"] for topic_input in existing_topics_input]
] ]
shout_topics_to_remove = session.query(ShoutTopic).filter( shout_topics_to_remove = session.query(ShoutTopic).filter(
and_( and_(
@ -144,21 +133,24 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
# Replace datetime with Unix timestamp # Replace datetime with Unix timestamp
current_time = int(time.time()) current_time = int(time.time())
shout_input["updated_at"] = current_time # Set updated_at as Unix timestamp shout_input["updated_at"] = current_time # Set updated_at as Unix timestamp
shout.update(shout_input) Shout.update(shout, shout_input)
session.add(shout)
updated = True updated = True
# TODO: use visibility setting # TODO: use visibility setting
if publish and shout.visibility == "authors": if publish and shout.visibility == ShoutVisibility.AUTHORS:
shout.visibility = "community" shout.visibility = ShoutVisibility.COMMUNITY
shout.published_at = current_time # Set published_at as Unix timestamp shout.published_at = current_time # Set published_at as Unix timestamp
session.add(shout)
updated = True updated = True
# notify on publish # notify on publish
notify_shout(shout.dict()) await notify_shout(shout.dict(), "public")
if updated: if updated:
session.commit() session.commit()
# GitTask(inp, user.username, user.email, "update shout %s" % slug) if not publish:
notify_shout(shout.dict(), "update") await notify_shout(shout.dict(), "update")
return {"shout": shout} return {"shout": shout}
@mutation.field("deleteShout") @mutation.field("deleteShout")
@login_required @login_required
async def delete_shout(_, info, shout_id): async def delete_shout(_, info, shout_id):
@ -175,5 +167,5 @@ async def delete_shout(_, info, shout_id):
current_time = int(time.time()) current_time = int(time.time())
shout.deleted_at = current_time # Set deleted_at as Unix timestamp shout.deleted_at = current_time # Set deleted_at as Unix timestamp
session.commit() session.commit()
notify_shout(shout.dict(), "delete") await notify_shout(shout.dict(), "delete")
return {} return {}

View File

@ -1,9 +1,10 @@
import time import time
from typing import List
from sqlalchemy import and_, asc, desc, select, text, func, case from sqlalchemy import and_, asc, desc, select, text, func, case
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from services.notify import notify_reaction from services.notify import notify_reaction
from services.auth import login_required from services.auth import login_required
from base.exceptions import OperationNotAllowed
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.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
@ -14,13 +15,9 @@ from orm.author import Author
def add_reaction_stat_columns(q): def add_reaction_stat_columns(q):
aliased_reaction = aliased(Reaction) aliased_reaction = aliased(Reaction)
q = q.outerjoin( q = q.outerjoin(aliased_reaction, Reaction.id == aliased_reaction.reply_to).add_columns(
aliased_reaction, Reaction.id == aliased_reaction.reply_to
).add_columns(
func.sum(aliased_reaction.id).label("reacted_stat"), func.sum(aliased_reaction.id).label("reacted_stat"),
func.sum(case((aliased_reaction.body.is_not(None), 1), else_=0)).label( func.sum(case((aliased_reaction.body.is_not(None), 1), else_=0)).label("commented_stat"),
"commented_stat"
),
func.sum( func.sum(
case( case(
(aliased_reaction.kind == ReactionKind.AGREE, 1), (aliased_reaction.kind == ReactionKind.AGREE, 1),
@ -56,9 +53,7 @@ def reactions_follow(author_id, shout_id: int, auto=False):
) )
if not following: if not following:
following = ShoutReactionsFollower.create( following = ShoutReactionsFollower(follower=author_id, shout=shout.id, auto=auto)
follower=author_id, shout=shout.id, auto=auto
)
session.add(following) session.add(following)
session.commit() session.commit()
return True return True
@ -109,11 +104,9 @@ def check_to_publish(session, author_id, reaction):
ReactionKind.LIKE, ReactionKind.LIKE,
ReactionKind.PROOF, ReactionKind.PROOF,
]: ]:
if is_published_author(author_id): if is_published_author(session, author_id):
# now count how many approvers are voted already # now count how many approvers are voted already
approvers_reactions = ( approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
session.query(Reaction).where(Reaction.shout == reaction.shout).all()
)
approvers = [ approvers = [
author_id, author_id,
] ]
@ -134,9 +127,7 @@ def check_to_hide(session, reaction):
ReactionKind.DISPROOF, ReactionKind.DISPROOF,
]: ]:
# if is_published_author(author_id): # if is_published_author(author_id):
approvers_reactions = ( approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
session.query(Reaction).where(Reaction.shout == reaction.shout).all()
)
rejects = 0 rejects = 0
for r in approvers_reactions: for r in approvers_reactions:
if r.kind in [ if r.kind in [
@ -188,12 +179,10 @@ async def create_reaction(_, info, reaction):
) )
if existing_reaction is not None: if existing_reaction is not None:
raise OperationNotAllowed("You can't vote twice") return {"error": "You can't vote twice"}
opposite_reaction_kind = ( opposite_reaction_kind = (
ReactionKind.DISLIKE ReactionKind.DISLIKE if reaction["kind"] == ReactionKind.LIKE.name else ReactionKind.LIKE
if reaction["kind"] == ReactionKind.LIKE.name
else ReactionKind.LIKE
) )
opposite_reaction = ( opposite_reaction = (
session.query(Reaction) session.query(Reaction)
@ -211,17 +200,11 @@ async def create_reaction(_, info, reaction):
if opposite_reaction is not None: if opposite_reaction is not None:
session.delete(opposite_reaction) session.delete(opposite_reaction)
r = Reaction.create(**reaction) r = Reaction(**reaction)
# Proposal accepting logix # Proposal accepting logix
if ( if r.reply_to is not None and r.kind == ReactionKind.ACCEPT and author_id in shout.dict()["authors"]:
r.reply_to is not None replied_reaction = session.query(Reaction).where(Reaction.id == r.reply_to).first()
and r.kind == ReactionKind.ACCEPT
and author_id in shout.dict()["authors"]
):
replied_reaction = (
session.query(Reaction).where(Reaction.id == r.reply_to).first()
)
if replied_reaction and replied_reaction.kind == ReactionKind.PROPOSE: if replied_reaction and replied_reaction.kind == ReactionKind.PROPOSE:
if replied_reaction.range: if replied_reaction.range:
old_body = shout.body old_body = shout.body
@ -230,7 +213,6 @@ async def create_reaction(_, info, reaction):
end = int(end) end = int(end)
new_body = old_body[:start] + replied_reaction.body + old_body[end:] new_body = old_body[:start] + replied_reaction.body + old_body[end:]
shout.body = new_body shout.body = new_body
# TODO: update git version control
session.add(r) session.add(r)
session.commit() session.commit()
@ -253,37 +235,39 @@ async def create_reaction(_, info, reaction):
rdict["stat"] = {"commented": 0, "reacted": 0, "rating": 0} rdict["stat"] = {"commented": 0, "reacted": 0, "rating": 0}
# notification call # notifications call
notify_reaction(rdict) await notify_reaction(rdict, "create")
return {"reaction": rdict} return {"reaction": rdict}
@mutation.field("updateReaction") @mutation.field("updateReaction")
@login_required @login_required
async def update_reaction(_, info, rid, reaction={}): async def update_reaction(_, info, rid, reaction):
author_id = info.context["author_id"] author_id = info.context["author_id"]
with local_session() as session: with local_session() as session:
q = select(Reaction).filter(Reaction.id == rid) q = select(Reaction).filter(Reaction.id == rid)
q = add_reaction_stat_columns(q) q = add_reaction_stat_columns(q)
q = q.group_by(Reaction.id) q = q.group_by(Reaction.id)
[r, reacted_stat, commented_stat, rating_stat] = ( [r, reacted_stat, commented_stat, rating_stat] = session.execute(q).unique().one()
session.execute(q).unique().one()
)
if not r: if not r:
return {"error": "invalid reaction id"} return {"error": "invalid reaction id"}
if r.created_by != author_id: if r.created_by != author_id:
return {"error": "access denied"} return {"error": "access denied"}
body = reaction.get("body")
r.body = reaction["body"] if body:
r.body = body
r.updated_at = int(time.time()) r.updated_at = int(time.time())
if r.kind != reaction["kind"]: if r.kind != reaction["kind"]:
# NOTE: change mind detection can be here # NOTE: change mind detection can be here
pass pass
# FIXME: range is not stable after body editing
if reaction.get("range"): if reaction.get("range"):
r.range = reaction.get("range") r.range = reaction.get("range")
session.commit() session.commit()
r.stat = { r.stat = {
"commented": commented_stat, "commented": commented_stat,
@ -291,7 +275,7 @@ async def update_reaction(_, info, rid, reaction={}):
"rating": rating_stat, "rating": rating_stat,
} }
notify_reaction(r.dict(), "update") await notify_reaction(r.dict(), "update")
return {"reaction": r} return {"reaction": r}
@ -313,7 +297,7 @@ async def delete_reaction(_, info, rid):
r.deleted_at = int(time.time()) r.deleted_at = int(time.time())
session.commit() session.commit()
notify_reaction(r.dict(), "delete") await notify_reaction(r.dict(), "delete")
return {"reaction": r} return {"reaction": r}
@ -391,25 +375,31 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
reaction.kind = reaction.kind.name reaction.kind = reaction.kind.name
reactions.append(reaction) reactions.append(reaction)
# ? # sort if by stat is present
if by.get("stat"): if by.get("stat"):
reactions.sort(lambda r: r.stat.get(by["stat"]) or r.created_at) reactions = sorted(reactions, key=lambda r: r.stat.get(by["stat"]) or r.created_at)
return reactions return reactions
def reacted_shouts_updates(follower_id):
shouts = []
with local_session() as session:
author = session.query(Author).where(Author.id == follower_id).first()
if author:
shouts = (
session.query(Reaction.shout)
.join(Shout)
.filter(Reaction.created_by == author.id)
.filter(Reaction.created_at > author.last_seen)
.all()
)
return shouts
@login_required @login_required
@query.field("followedReactions") @query.field("followedReactions")
async def followed_reactions(_, info): async def get_reacted_shouts(_, info) -> List[Shout]:
author_id = info.context["author_id"] author_id = info.context["author_id"]
# FIXME: method should return array of shouts shouts = reacted_shouts_updates(author_id)
with local_session() as session: return shouts
author = session.query(Author).where(Author.id == author_id).first()
reactions = (
session.query(Reaction.shout)
.where(Reaction.created_by == author.id)
.filter(Reaction.created_at > author.last_seen)
.all()
)
return reactions

View File

@ -1,5 +1,4 @@
import time import time
from aiohttp.web_exceptions import HTTPException
from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc, asc, select, func, case, and_, nulls_last from sqlalchemy.sql.expression import desc, asc, select, func, case, and_, nulls_last
@ -9,7 +8,7 @@ from orm.topic import TopicFollower
from orm.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.author import AuthorFollower from orm.author import AuthorFollower
from servies.viewed import ViewedStorage from services.viewed import ViewedStorage
def add_stat_columns(q): def add_stat_columns(q):
@ -113,7 +112,7 @@ async def load_shout(_, _info, slug=None, shout_id=None):
author.caption = author_caption.caption author.caption = author_caption.caption
return shout return shout
except Exception: except Exception:
raise HTTPException(status_code=404, detail="Slug was not found: %s" % slug) return None
async def load_shouts_by(_, info, options): async def load_shouts_by(_, info, options):

View File

@ -3,7 +3,7 @@ from sqlalchemy.orm import aliased
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 resolvers import mutation, query from services.schema import mutation, query
from orm.shout import ShoutTopic, ShoutAuthor from orm.shout import ShoutTopic, ShoutAuthor
from orm.topic import Topic, TopicFollower from orm.topic import Topic, TopicFollower
from orm.author import Author from orm.author import Author
@ -12,9 +12,7 @@ from orm.author import Author
async def followed_topics(follower_id): async def followed_topics(follower_id):
q = select(Author) q = select(Author)
q = add_topic_stat_columns(q) q = add_topic_stat_columns(q)
q = q.join(TopicFollower, TopicFollower.author == Author.id).where( q = q.join(TopicFollower, TopicFollower.author == Author.id).where(TopicFollower.follower == follower_id)
TopicFollower.follower == follower_id
)
# Pass the query to the get_authors_from_query function and return the results # Pass the query to the get_authors_from_query function and return the results
return get_topics_from_query(q) return get_topics_from_query(q)
@ -27,15 +25,9 @@ def add_topic_stat_columns(q):
q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic) 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) .outerjoin(aliased_shout_author, ShoutTopic.shout == aliased_shout_author.shout)
.add_columns( .add_columns(func.count(distinct(aliased_shout_author.user)).label("authors_stat"))
func.count(distinct(aliased_shout_author.user)).label("authors_stat")
)
.outerjoin(aliased_topic_follower) .outerjoin(aliased_topic_follower)
.add_columns( .add_columns(func.count(distinct(aliased_topic_follower.follower)).label("followers_stat"))
func.count(distinct(aliased_topic_follower.follower)).label(
"followers_stat"
)
)
) )
q = q.group_by(Topic.id) q = q.group_by(Topic.id)
@ -111,7 +103,7 @@ async def get_topic(_, _info, slug):
async def create_topic(_, _info, inp): async def create_topic(_, _info, inp):
with local_session() as session: with local_session() as session:
# TODO: check user permissions to create topic for exact community # TODO: check user permissions to create topic for exact community
new_topic = Topic.create(**inp) new_topic = Topic(**inp)
session.add(new_topic) session.add(new_topic)
session.commit() session.commit()
@ -126,7 +118,8 @@ async def update_topic(_, _info, inp):
if not topic: if not topic:
return {"error": "topic not found"} return {"error": "topic not found"}
else: else:
topic.update(**inp) Topic.update(topic, inp)
session.add(topic)
session.commit() session.commit()
return {"topic": topic} return {"topic": topic}
@ -136,7 +129,7 @@ def topic_follow(follower_id, slug):
try: try:
with local_session() as session: with local_session() as session:
topic = session.query(Topic).where(Topic.slug == slug).one() topic = session.query(Topic).where(Topic.slug == slug).one()
_following = TopicFollower.create(topic=topic.id, follower=follower_id) _following = TopicFollower(topic=topic.id, follower=follower_id)
return True return True
except Exception: except Exception:
return False return False

View File

@ -96,8 +96,8 @@ type Reaction {
body: String body: String
reply_to: Int reply_to: Int
stat: Stat stat: Stat
old_id: String oid: String
old_thread: String # old_thread: String
} }
type Shout { type Shout {
@ -212,7 +212,7 @@ input ReactionInput {
shout: Int! shout: Int!
range: String range: String
body: String body: String
reply_to: Int replyTo: Int
} }
input AuthorsBy { input AuthorsBy {
@ -266,7 +266,7 @@ input ReactionBy {
search: String search: String
comment: Boolean comment: Boolean
topic: String topic: String
created_by: String created_by: Int
days: Int days: Int
sort: String sort: String
} }

View File

@ -1,35 +1,41 @@
from contextlib import contextmanager # from contextlib import contextmanager
import logging from typing import Any, Callable, Dict, TypeVar
from typing import TypeVar, Any, Dict, Generic, Callable
from sqlalchemy import create_engine, Column, Integer # from psycopg2.errors import UniqueViolation
from sqlalchemy import Column, Integer, create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import Session
from sqlalchemy.sql.schema import Table from sqlalchemy.sql.schema import Table
from settings import DB_URL from settings import DB_URL
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
engine = create_engine(DB_URL, echo=False, pool_size=10, max_overflow=20) engine = create_engine(DB_URL, echo=False, pool_size=10, max_overflow=20)
Session = sessionmaker(bind=engine, expire_on_commit=False)
T = TypeVar("T") T = TypeVar("T")
REGISTRY: Dict[str, type] = {} REGISTRY: Dict[str, type] = {}
@contextmanager # @contextmanager
def local_session(): def local_session(src=""):
session = Session() return Session(bind=engine, expire_on_commit=False)
try:
yield session # try:
session.commit() # yield session
except Exception as e: # session.commit()
print(f"[services.db] Error session: {e}") # except Exception as e:
session.rollback() # if not (src == "create_shout" and isinstance(e, UniqueViolation)):
raise # import traceback
finally:
session.close() # session.rollback()
# print(f"[services.db] {src}: {e}")
# traceback.print_exc()
# raise Exception("[services.db] exception")
# finally:
# session.close()
class Base(declarative_base()): class Base(declarative_base()):
@ -46,38 +52,17 @@ class Base(declarative_base()):
def __init_subclass__(cls, **kwargs): def __init_subclass__(cls, **kwargs):
REGISTRY[cls.__name__] = cls REGISTRY[cls.__name__] = cls
@classmethod
def create(cls: Generic[T], **kwargs) -> Generic[T]:
try:
instance = cls(**kwargs)
return instance.save()
except Exception as e:
print(f"[services.db] Error create: {e}")
return None
def save(self) -> Generic[T]:
with local_session() as session:
try:
session.add(self)
except Exception as e:
print(f"[services.db] Error save: {e}")
return self
def update(self, input):
column_names = self.__table__.columns.keys()
for name, value in input.items():
if name in column_names:
setattr(self, name, value)
with local_session() as session:
try:
session.commit()
except Exception as e:
print(f"[services.db] Error update: {e}")
def dict(self) -> Dict[str, Any]: def dict(self) -> Dict[str, Any]:
column_names = self.__table__.columns.keys() column_names = self.__table__.columns.keys()
if "_sa_instance_state" in column_names:
column_names.remove("_sa_instance_state")
try: try:
return {c: getattr(self, c) for c in column_names} return {c: getattr(self, c) for c in column_names}
except Exception as e: except Exception as e:
print(f"[services.db] Error dict: {e}") print(f"[services.db] Error dict: {e}")
return {} return {}
def update(self, values: Dict[str, Any]) -> None:
for key, value in values.items():
if hasattr(self, key):
setattr(self, key, value)

View File

@ -1,6 +1,7 @@
import redis.asyncio as aredis import redis.asyncio as aredis
from settings import REDIS_URL from settings import REDIS_URL
class RedisCache: class RedisCache:
def __init__(self, uri=REDIS_URL): def __init__(self, uri=REDIS_URL):
self._uri: str = uri self._uri: str = uri
@ -11,28 +12,25 @@ class RedisCache:
self._client = aredis.Redis.from_url(self._uri, decode_responses=True) self._client = aredis.Redis.from_url(self._uri, decode_responses=True)
async def disconnect(self): async def disconnect(self):
await self._client.aclose() if self._client:
await self._client.close()
async def execute(self, command, *args, **kwargs): async def execute(self, command, *args, **kwargs):
if not self._client: if self._client:
await self.connect() try:
try: print("[redis] " + command + " " + " ".join(args))
print(f"[redis] {command} {args}") r = await self._client.execute_command(command, *args, **kwargs)
return await self._client.execute_command(command, *args, **kwargs) return r
except Exception as e: except Exception as e:
print(f"[redis] ERROR: {e} with: {command} {args}") print(f"[redis] error: {e}")
import traceback
traceback.print_exc()
return None return None
async def subscribe(self, *channels): async def subscribe(self, *channels):
if not self._client: if self._client:
await self.connect() async with self._client.pubsub() as pubsub:
async with self._client.pubsub() as pubsub: for channel in channels:
for channel in channels: await pubsub.subscribe(channel)
await pubsub.subscribe(channel) self.pubsub_channels.append(channel)
self.pubsub_channels.append(channel)
async def unsubscribe(self, *channels): async def unsubscribe(self, *channels):
if not self._client: if not self._client:
@ -48,12 +46,15 @@ class RedisCache:
await self._client.publish(channel, data) await self._client.publish(channel, data)
async def lrange(self, key, start, stop): async def lrange(self, key, start, stop):
print(f"[redis] LRANGE {key} {start} {stop}") if self._client:
return await self._client.lrange(key, start, stop) print(f"[redis] LRANGE {key} {start} {stop}")
return await self._client.lrange(key, start, stop)
async def mget(self, key, *keys): async def mget(self, key, *keys):
print(f"[redis] MGET {key} {keys}") if self._client:
return await self._client.mget(key, *keys) print(f"[redis] MGET {key} {keys}")
return await self._client.mget(key, *keys)
redis = RedisCache() redis = RedisCache()

View File

@ -11,13 +11,14 @@ def serialize_datetime(value):
return value.isoformat() return value.isoformat()
@query.field("_service") # NOTE: was used by studio
def resolve_service(*_): # @query.field("_service")
# Load the full SDL from your SDL file # def resolve_service(*_):
with open("schemas/core.graphql", "r") as file: # # Load the full SDL from your SDL file
full_sdl = file.read() # with open("schemas/core.graphql", "r") as file:
# full_sdl = file.read()
return {"sdl": full_sdl} #
# return {"sdl": full_sdl}
resolvers = [query, mutation, datetime_scalar] resolvers = [query, mutation, datetime_scalar]