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]
- services: db context manager
- services: ViewedStorage fixes

View File

@ -25,17 +25,33 @@ apt install redis nginx
Then run nginx, redis and API server
```
redis-server
poetry env use 3.12
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.
# 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
from sqlalchemy import JSON as JSONType
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from services.db import Base

View File

@ -1,5 +1,7 @@
import time
from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy import Column, ForeignKey, Integer, String
from services.db import Base
@ -20,4 +22,4 @@ class Collection(Base):
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")
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
from sqlalchemy import Column, String, ForeignKey, Integer
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from services.db import Base, local_session
from orm.author import Author
from services.db import Base, local_session
class CommunityAuthor(Base):
@ -29,10 +30,12 @@ class Community(Base):
@staticmethod
def init_table():
with local_session() as session:
with local_session("orm.community") as session:
d = session.query(Community).filter(Community.slug == "discours").first()
if not d:
d = Community.create(name="Дискурс", slug="discours")
print("[orm] created community %s" % d.slug)
d = Community(name="Дискурс", slug="discours")
session.add(d)
session.commit()
print("[orm.community] created community %s" % d.slug)
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
from enum import Enum as Enumeration
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
from services.db import Base
class ReactionKind(Enumeration):
@ -33,5 +35,7 @@ class Reaction(Base):
deleted_by = Column(ForeignKey("author.id"), nullable=True, index=True)
shout = Column(ForeignKey("shout.id"), nullable=False, index=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)
oid = Column(String)

View File

@ -1,20 +1,14 @@
import time
from enum import Enum as Enumeration
from sqlalchemy import (
Enum,
Boolean,
Column,
ForeignKey,
Integer,
String,
JSON,
)
from sqlalchemy.orm import column_property, relationship
from services.db import Base, local_session
from sqlalchemy import JSON, Boolean, Column, Enum, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from orm.author import Author
from orm.community import Community
from orm.reaction import Reaction
from orm.topic import Topic
from orm.author import Author
from services.db import Base
class ShoutTopic(Base):
@ -67,7 +61,6 @@ class Shout(Base):
published_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)
body = Column(String, nullable=False, comment="Body")
@ -80,9 +73,9 @@ class Shout(Base):
layout = Column(String, nullable=True)
media = Column(JSON, nullable=True)
authors = relationship(lambda: Author, secondary=ShoutAuthor.__tablename__)
topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__)
communities = relationship(lambda: Community, secondary=ShoutCommunity.__tablename__)
authors = relationship(lambda: Author, secondary="shout_author")
topics = relationship(lambda: Topic, secondary="shout_topic")
communities = relationship(lambda: Community, secondary="shout_community")
reactions = relationship(lambda: Reaction)
visibility = Column(Enum(ShoutVisibility), default=ShoutVisibility.AUTHORS)
@ -91,15 +84,3 @@ class Shout(Base):
version_of = Column(ForeignKey("shout.id"), 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
from sqlalchemy import Boolean, Column, Integer, ForeignKey, String
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
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]
name = "discoursio-core"
version = "0.2.13"
version = "0.2.14"
description = "core module for discours.io"
authors = ["discoursio devteam"]
license = "MIT"
@ -11,7 +11,7 @@ python = "^3.12"
SQLAlchemy = "^2.0.22"
httpx = "^0.25.0"
redis = {extras = ["hiredis"], version = "^5.0.1"}
uvicorn = "^0.23.2"
uvicorn = "^0.24"
sentry-sdk = "^1.32.0"
gql = {git = "https://github.com/graphql-python/gql.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 community import followed_communities
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):
@ -28,16 +28,13 @@ def add_author_stat_columns(q):
func.count(distinct(followers_table.follower)).label("followers_stat")
)
q = q.outerjoin(
followings_table, followings_table.follower == Author.id
).add_columns(
q = q.outerjoin(followings_table, followings_table.follower == Author.id).add_columns(
func.count(distinct(followings_table.author)).label("followings_stat")
)
q = q.add_columns(literal(0).label("rating_stat"))
# FIXME
# 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')
# )
@ -83,16 +80,10 @@ def get_authors_from_query(q):
async def author_followings(author_id: int):
return {
"unread": await get_total_unread_counter(author_id), # unread inbox messages counter
"topics": [
t.slug for t in await followed_topics(author_id)
], # followed topics slugs
"authors": [
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
"topics": [t.slug for t in await followed_topics(author_id)], # followed topics slugs
"authors": [a.slug for a in await followed_authors(author_id)], # followed authors slugs
"reactions": [s.slug for s in await followed_reactions(author_id)], # fresh reacted shouts slugs
"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"]
with local_session() as session:
author = session.query(Author).where(Author.id == author_id).first()
author.update(profile)
Author.update(author, profile)
session.add(author)
session.commit()
return {"error": None, "author": author}
@ -112,7 +104,7 @@ def author_follow(follower_id, slug):
try:
with local_session() as session:
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.commit()
return True
@ -163,12 +155,7 @@ async def load_authors_by(_, _info, by, limit, offset):
elif by.get("name"):
q = q.filter(Author.name.ilike(f"%{by['name']}%"))
elif by.get("topic"):
q = (
q.join(ShoutAuthor)
.join(ShoutTopic)
.join(Topic)
.where(Topic.slug == by["topic"])
)
q = q.join(ShoutAuthor).join(ShoutTopic).join(Topic).where(Topic.slug == by["topic"])
if by.get("last_seen"): # in unixtime
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):
q = select(Author)
q = add_author_stat_columns(q)
q = q.join(AuthorFollower, AuthorFollower.author == Author.id).where(
AuthorFollower.follower == follower_id
)
q = q.join(AuthorFollower, AuthorFollower.author == Author.id).where(AuthorFollower.follower == follower_id)
# Pass the query to the get_authors_from_query function and return the results
return get_authors_from_query(q)
@ -226,20 +211,19 @@ async def rate_author(_, info, rated_user_id, value):
with local_session() as session:
rating = (
session.query(AuthorRating)
.filter(
and_(
AuthorRating.rater == author_id,
AuthorRating.user == rated_user_id
)
)
.filter(and_(AuthorRating.rater == author_id, AuthorRating.user == rated_user_id))
.first()
)
if rating:
rating.value = value
session.add(rating)
session.commit()
return {}
else:
try:
AuthorRating.create(rater=author_id, user=rated_user_id, value=value)
rating = AuthorRating(rater=author_id, user=rated_user_id, value=value)
session.add(rating)
session.commit()
except Exception as err:
return {"error": err}
return {}

View File

@ -14,9 +14,7 @@ def add_community_stat_columns(q):
q = q.outerjoin(shout_community_aliased).add_columns(
func.count(distinct(shout_community_aliased.shout)).label("shouts_stat")
)
q = q.outerjoin(
community_followers, community_followers.author == Author.id
).add_columns(
q = q.outerjoin(community_followers, community_followers.author == Author.id).add_columns(
func.count(distinct(community_followers.follower)).label("followers_stat")
)
@ -75,7 +73,7 @@ def community_follow(follower_id, slug):
try:
with local_session() as session:
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.commit()
return True

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import time
from aiohttp.web_exceptions import HTTPException
from sqlalchemy.orm import joinedload, aliased
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.shout import Shout, ShoutAuthor, ShoutTopic
from orm.author import AuthorFollower
from servies.viewed import ViewedStorage
from services.viewed import ViewedStorage
def add_stat_columns(q):
@ -113,7 +112,7 @@ async def load_shout(_, _info, slug=None, shout_id=None):
author.caption = author_caption.caption
return shout
except Exception:
raise HTTPException(status_code=404, detail="Slug was not found: %s" % slug)
return None
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.db import local_session
from resolvers 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
@ -12,9 +12,7 @@ from orm.author import Author
async def followed_topics(follower_id):
q = select(Author)
q = add_topic_stat_columns(q)
q = q.join(TopicFollower, TopicFollower.author == Author.id).where(
TopicFollower.follower == follower_id
)
q = q.join(TopicFollower, TopicFollower.author == Author.id).where(TopicFollower.follower == follower_id)
# Pass the query to the get_authors_from_query function and return the results
return get_topics_from_query(q)
@ -27,15 +25,9 @@ def add_topic_stat_columns(q):
q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic)
.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.user)).label("authors_stat")
)
.add_columns(func.count(distinct(aliased_shout_author.user)).label("authors_stat"))
.outerjoin(aliased_topic_follower)
.add_columns(
func.count(distinct(aliased_topic_follower.follower)).label(
"followers_stat"
)
)
.add_columns(func.count(distinct(aliased_topic_follower.follower)).label("followers_stat"))
)
q = q.group_by(Topic.id)
@ -111,7 +103,7 @@ async def get_topic(_, _info, slug):
async def create_topic(_, _info, inp):
with local_session() as session:
# TODO: check user permissions to create topic for exact community
new_topic = Topic.create(**inp)
new_topic = Topic(**inp)
session.add(new_topic)
session.commit()
@ -126,7 +118,8 @@ async def update_topic(_, _info, inp):
if not topic:
return {"error": "topic not found"}
else:
topic.update(**inp)
Topic.update(topic, inp)
session.add(topic)
session.commit()
return {"topic": topic}
@ -136,7 +129,7 @@ def topic_follow(follower_id, slug):
try:
with local_session() as session:
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
except Exception:
return False

View File

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

View File

@ -1,35 +1,41 @@
from contextlib import contextmanager
import logging
from typing import TypeVar, Any, Dict, Generic, Callable
from sqlalchemy import create_engine, Column, Integer
# from contextlib import contextmanager
from typing import Any, Callable, Dict, TypeVar
# from psycopg2.errors import UniqueViolation
from sqlalchemy import Column, Integer, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import Session
from sqlalchemy.sql.schema import Table
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)
Session = sessionmaker(bind=engine, expire_on_commit=False)
T = TypeVar("T")
REGISTRY: Dict[str, type] = {}
@contextmanager
def local_session():
session = Session()
try:
yield session
session.commit()
except Exception as e:
print(f"[services.db] Error session: {e}")
session.rollback()
raise
finally:
session.close()
# @contextmanager
def local_session(src=""):
return Session(bind=engine, expire_on_commit=False)
# try:
# yield session
# session.commit()
# except Exception as e:
# if not (src == "create_shout" and isinstance(e, UniqueViolation)):
# import traceback
# session.rollback()
# print(f"[services.db] {src}: {e}")
# traceback.print_exc()
# raise Exception("[services.db] exception")
# finally:
# session.close()
class Base(declarative_base()):
@ -46,38 +52,17 @@ class Base(declarative_base()):
def __init_subclass__(cls, **kwargs):
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]:
column_names = self.__table__.columns.keys()
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:
print(f"[services.db] Error dict: {e}")
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
from settings import REDIS_URL
class RedisCache:
def __init__(self, uri=REDIS_URL):
self._uri: str = uri
@ -11,24 +12,21 @@ class RedisCache:
self._client = aredis.Redis.from_url(self._uri, decode_responses=True)
async def disconnect(self):
await self._client.aclose()
if self._client:
await self._client.close()
async def execute(self, command, *args, **kwargs):
if not self._client:
await self.connect()
if self._client:
try:
print(f"[redis] {command} {args}")
return await self._client.execute_command(command, *args, **kwargs)
print("[redis] " + command + " " + " ".join(args))
r = await self._client.execute_command(command, *args, **kwargs)
return r
except Exception as e:
print(f"[redis] ERROR: {e} with: {command} {args}")
import traceback
traceback.print_exc()
print(f"[redis] error: {e}")
return None
async def subscribe(self, *channels):
if not self._client:
await self.connect()
if self._client:
async with self._client.pubsub() as pubsub:
for channel in channels:
await pubsub.subscribe(channel)
@ -48,13 +46,16 @@ class RedisCache:
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()
__all__ = ["redis"]

View File

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