Compare commits

...

14 Commits
master ... dev

Author SHA1 Message Date
Keith Edmunds
d40579e695 Added user reports 2023-01-21 23:37:16 +00:00
Keith Edmunds
0c23956dd2 Report on hashtags, followed and unfollowed, complete. 2023-01-21 23:25:50 +00:00
Keith Edmunds
c7757efbf6 Untracked hashtags report 2023-01-21 23:11:35 +00:00
Keith Edmunds
b15c1029ef Tests run, database populates 2023-01-18 21:23:23 +00:00
Keith Edmunds
66d9c7c6b5 First pass processing tests run OK 2023-01-17 18:25:55 +00:00
Keith Edmunds
e14177c069 First pass of model tests complete 2023-01-15 21:19:18 +00:00
Keith Edmunds
d8f0beec43 Change to CLI; introduce tests 2023-01-15 20:50:57 +00:00
Keith Edmunds
b797746229 Mass loading but overrunning API limits 2023-01-10 17:09:06 +00:00
Keith Edmunds
e6d8f10fe3 Posts display and step forward, boosted posts handled. 2023-01-07 23:25:48 +00:00
Keith Edmunds
f034ef4f56 Restarted alembic 2023-01-06 20:45:16 +00:00
Keith Edmunds
a25c6819ff Updating accounts and hashtags in db from Mastodon works 2023-01-06 20:42:56 +00:00
Keith Edmunds
aab8486549 UI displays 2023-01-04 17:02:53 +00:00
Keith Edmunds
a0da5bf218 Import from dict apparently all working 2023-01-03 23:18:11 +00:00
Keith Edmunds
c0ccea1dde Loads from saved dict
Creates and links hashtags
Creates and linke accounts
2023-01-02 22:41:17 +00:00
44 changed files with 4011 additions and 451 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ tags
Session.vim Session.vim
.direnv .direnv
.envrc .envrc
testdata/

View File

@ -4,11 +4,20 @@
# path to migration scripts # path to migration scripts
script_location = migrations script_location = migrations
# template used to generate migration files # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# file_template = %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# timezone to use when rendering the date # sys.path path, will be prepended to sys.path if present.
# within the migration file as well as the filename. # defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz() # string value is passed to dateutil.tz.gettz()
# leave blank for localtime # leave blank for localtime
# timezone = # timezone =
@ -26,10 +35,21 @@ script_location = migrations
# versions/ directory # versions/ directory
# sourceless = false # sourceless = false
# version location specification; this defaults # version location specification; This defaults
# to migrations/versions. When using multiple version # to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path # directories, initial revisions must be specified with --version-path.
# version_locations = %(here)s/bar %(here)s/bat migrations/versions # The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files # the output encoding used when revision files
# are written from script.py.mako # are written from script.py.mako
@ -44,10 +64,10 @@ sqlalchemy.url = driver://user:pass@localhost/dbname
# detail and examples # detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint # format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black # hooks = black
# black.type=console_scripts # black.type = console_scripts
# black.entrypoint=black # black.entrypoint = black
# black.options=-l 79 # black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration # Logging configuration
[loggers] [loggers]

View File

@ -7,15 +7,19 @@ class Config(object):
# KAEID = 109568725613662482 # KAEID = 109568725613662482
# DEBUG_FUNCTIONS: List[Optional[str]] = [] # DEBUG_FUNCTIONS: List[Optional[str]] = []
# DEBUG_MODULES: List[Optional[str]] = ['dbconfig'] # DEBUG_MODULES: List[Optional[str]] = ['dbconfig']
DISPLAY_SQL = True DISPLAY_SQL = False
# ERRORS_FROM = ['noreply@midnighthax.com'] ERRORS_FROM = ['noreply@midnighthax.com']
# ERRORS_TO = ['kae@midnighthax.com'] ERRORS_TO = ['kae@midnighthax.com']
LOG_LEVEL_STDERR = logging.ERROR LOG_LEVEL_STDERR = logging.ERROR
LOG_LEVEL_SYSLOG = logging.DEBUG LOG_LEVEL_SYSLOG = logging.DEBUG
LOG_NAME = "urma" LOG_NAME = "urma"
# MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
# MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25) MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
# MAIL_SERVER = os.environ.get('MAIL_SERVER') or MAIL_SERVER = os.environ.get('MAIL_SERVER') or "woodlands.midnighthax.com"
# "woodlands.midnighthax.com" MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
# MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
# MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None MAX_POSTS_TO_FETCH = 200
POINTS_BOOSTED = 1
POINTS_FAVOURITED = 1
TOP_HASHTAGS_TO_REPORT = 3
TOP_POSTS_TO_REPORT = 3

View File

@ -2,18 +2,26 @@ import os
import smtplib import smtplib
import ssl import ssl
from config import Config
from email.message import EmailMessage from email.message import EmailMessage
from config import Config from config import Config
from log import log from log import log
from typing import Any, List
def ask_yes_no(title: str, question: str) -> bool:
"""Ask question; return True for yes, False for no"""
button_reply = QMessageBox.question(None, title, question) def index_ojects_by_parameter(object_list: List, param: Any):
"""
Create a dictionary from passed list where each list entry is keyed
by passed param
"""
return button_reply == QMessageBox.Yes results = {}
for obj in object_list:
results[getattr(obj, param)] = obj
return results
def send_mail(to_addr, from_addr, subj, body): def send_mail(to_addr, from_addr, subj, body):
@ -40,15 +48,3 @@ def send_mail(to_addr, from_addr, subj, body):
print(e) print(e)
finally: finally:
s.quit() s.quit()
def show_OK(title: str, msg: str) -> None:
"""Display a message to user"""
QMessageBox.information(None, title, msg, buttons=QMessageBox.Ok)
def show_warning(title: str, msg: str) -> None:
"""Display a warning to user"""
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)

View File

@ -76,7 +76,7 @@ def log_uncaught_exceptions(_ex_cls, ex, tb):
logging.critical(''.join(traceback.format_tb(tb))) logging.critical(''.join(traceback.format_tb(tb)))
print("\033[1;37;40m") print("\033[1;37;40m")
print(stackprinter.format(ex, style="darkbg2", add_summary=True)) print(stackprinter.format(ex, style="darkbg2", add_summary=True))
if os.environ["MM_ENV"] != "DEVELOPMENT": if os.environ["URMA_ENV"] != "DEVELOPMENT":
msg = stackprinter.format(ex) msg = stackprinter.format(ex)
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
"Exception from musicmuster", msg) "Exception from musicmuster", msg)

View File

@ -4,12 +4,16 @@ import os.path
from dbconfig import Session, scoped_session from dbconfig import Session, scoped_session
from typing import List, Optional
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
Column, Column,
DateTime, DateTime,
ForeignKey, ForeignKey,
func,
Integer, Integer,
select,
String, String,
) )
@ -19,6 +23,9 @@ from sqlalchemy.orm import (
declarative_base, declarative_base,
relationship, relationship,
) )
from sqlalchemy.orm.exc import (
NoResultFound
)
from config import Config from config import Config
from log import log from log import log
@ -30,39 +37,60 @@ class Accounts(Base):
__tablename__ = 'accounts' __tablename__ = 'accounts'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
account_id = Column(Integer, index=True, nullable=False) account_id = Column(String(32), index=True, nullable=False)
username = Column(String(256), index=True, nullable=False) username = Column(String(256), index=True, default=None)
acct = Column(String(256), index=False, nullable=False) acct = Column(String(256), index=False, default=None)
display_name = Column(String(256), index=False, nullable=False) display_name = Column(String(256), index=False, default=None)
bot = Column(Boolean, index=False, nullable=False, default=False) bot = Column(Boolean, index=False, nullable=False, default=False)
url = Column(String(256), index=False) url = Column(String(256), index=False)
followed = Column(Boolean, index=False, nullable=False, default=False) followed = Column(Boolean, index=False, nullable=False, default=False)
posts = relationship("Posts", back_populates="account")
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<Accounts(id={self.id}, url={self.content[:60]}, " f"<Accounts(id={self.id}, username={self.username}, "
f"followed={self.followed}>" f"acct={self.acct}, followed={self.followed}>"
) )
def __init__(self, session: Session, account_id: str) -> None:
class Attachments(Base): self.account_id = account_id
__tablename__ = 'attachments'
id = Column(Integer, primary_key=True, autoincrement=True) session.add(self)
media_id = Column(Integer, index=True, nullable=False) session.flush()
media_type = Column(String(256), index=False)
url = Column(String(256), index=False)
preview_url = Column(String(256), index=False)
description = Column(String(2048), index=False)
posts = relationship("Posts", back_populates="media_attachments")
def __repr__(self) -> str: @classmethod
return ( def get_followed(cls, session: Session) -> List["Accounts"]:
f"<Attachments(id={self.id}, url={self.url}, " """
f"content={self.content[:60]}, followed={self.followed}>" Return a list of account objects that we follow
"""
records = (
session.execute(
select(cls)
.where(cls.followed.is_(True))
).scalars().all()
) )
return records
@classmethod
def get_or_create(cls, session: Session, account_id: str) -> "Accounts":
"""
Return any existing account with this id or create a new one
"""
try:
rec = (
session.execute(
select(cls)
.where(cls.account_id == account_id)
).scalar_one()
)
except NoResultFound:
rec = Accounts(session, account_id)
return rec
class Hashtags(Base): class Hashtags(Base):
__tablename__ = 'hashtags' __tablename__ = 'hashtags'
@ -70,52 +98,155 @@ class Hashtags(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(256), index=True, nullable=False) name = Column(String(256), index=True, nullable=False)
url = Column(String(256), index=False) url = Column(String(256), index=False)
posts = relationship("Posts", secondary="post_tags", backref="hashtags")
followed = Column(Boolean, index=False, nullable=False, default=False)
posttags = relationship("PostTags", back_populates="hashtag")
posts = association_proxy("posttags", "post")
tags_to_posts = relationship("PostTags", back_populates="hashtag")
posts = association_proxy("tags_to_posts", "post")
followed = Column(Boolean, index=False, nullable=False, default=False)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<Hashtags(id={self.id}, name={self.name}, " f"<Hashtags(id={self.id}, name={self.name}, "
f"url={self.url}>, followed={self.followed}>" f"url={self.url}, followed={self.followed}>"
) )
def __init__(self, session: Session, name: str, url: str) -> None:
self.name = name
self.url = url
session.add(self)
session.flush()
@classmethod
def get_all(cls, session: Session) -> List["Hashtags"]:
"""
Return a list of all hashtags
"""
records = (
session.execute(
select(cls)
).scalars().all()
)
return records
@classmethod
def get_followed(cls, session: Session) -> List["Hashtags"]:
"""
Return a list of hashtags objects that we follow
"""
records = (
session.execute(
select(cls)
.where(cls.followed.is_(True))
).scalars().all()
)
return records
@classmethod
def get_or_create(cls, session: Session,
name: str, url: str) -> "Hashtags":
"""
Return any existing hashtag with this name or create a new one
"""
try:
rec = (
session.execute(
select(cls)
.where(cls.name == name)
).scalar_one()
)
except NoResultFound:
rec = Hashtags(session, name, url)
return rec
class Posts(Base): class Posts(Base):
__tablename__ = 'posts' __tablename__ = 'posts'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
post_id = Column(Integer, index=True, nullable=False) post_id = Column(String(32), index=True, nullable=False)
account_id = Column(Integer, ForeignKey('accounts.id'), nullable=True)
account = relationship("Accounts", foreign_keys=[account_id])
created_at = Column(DateTime, index=True, default=None) created_at = Column(DateTime, index=True, default=None)
uri = Column(String(256), index=False) uri = Column(String(256), index=False)
url = Column(String(256), index=False)
content = Column(String(2048), index=False, default="")
account_id = Column(Integer, ForeignKey('accounts.id'), nullable=True)
account = relationship("Accounts", back_populates="posts")
parent_id = Column(Integer, ForeignKey("posts.id")) posts_to_tags = relationship("PostTags", back_populates="post")
reblog = relationship("Posts") hashtags = association_proxy("posts_to_tags", "hashtag")
media_attachments_id = Column(Integer, ForeignKey('attachments.id'), favourited = Column(Boolean, index=True, nullable=False, default=False)
nullable=True) bookmarked = Column(Boolean, index=True, nullable=False, default=False)
media_attachments = relationship("Attachments", back_populates="posts")
posttags = relationship("PostTags", back_populates="post")
rating = Column(Integer, index=True, default=None)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Posts(id={self.id}, content={self.content[:60]}>" return f"<Posts(id={self.id}>"
def __init__(self, session: Session, post_id) -> None:
self.post_id = post_id
session.add(self)
session.flush()
@classmethod
def get_by_post_id(cls, session: Session, post_id: str) -> "Posts":
"""
Return post identified by post_id or None
"""
return (
session.scalars(
select(cls)
.where(cls.post_id == post_id)
.limit(1)
).first()
)
@classmethod
def get_or_create(cls, session: Session, post_id: str) -> "Posts":
"""
Return any existing post with this id or create a new one
"""
try:
rec = (
session.execute(
select(cls)
.where(cls.post_id == post_id)
).scalar_one()
)
except NoResultFound:
rec = Posts(session, post_id)
return rec
@staticmethod
def max_post_id(session):
"""
Return the maximum post_id
"""
return session.scalars(select(func.max(Posts.post_id))).first()
class PostTags(Base): class PostTags(Base):
__tablename__ = 'post_tags' __tablename__ = 'post_tags'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
post_id = Column(Integer, ForeignKey('posts.id'), nullable=False,
index=True)
hashtag_id = Column(Integer, ForeignKey('hashtags.id'), nullable=False,
index=True)
post_id = Column(Integer, ForeignKey('posts.id'), nullable=False) post = relationship(Posts, back_populates="posts_to_tags")
post = relationship(Posts, back_populates="posttags") hashtag = relationship("Hashtags")
hashtag_id = Column(Integer, ForeignKey('hashtags.id'), nullable=False) def __init__(self, hashtag=None, post=None):
hashtag = relationship("Hashtags", back_populates="posttags") self.post = post
self.hashtag = hashtag

BIN
app/ui/dont-know-woman.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
app/ui/dont-know.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
app/ui/double-left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
app/ui/double-right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
app/ui/green-tick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

503
app/ui/main_window.ui Normal file
View File

@ -0,0 +1,503 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>709</width>
<height>1100</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">background-color: #232834;</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QTextEdit" name="txtPost">
<property name="geometry">
<rect>
<x>10</x>
<y>90</y>
<width>351</width>
<height>671</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>341</width>
<height>181</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(154, 153, 150); border-radius: 10px;
</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustIgnored</enum>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; color:#ffffff;&quot;&gt;The magic of adulthood is in finding new and interesting ways of being disappointed &lt;/span&gt;&lt;span style=&quot; color:#5e5c64;&quot;&gt;&amp;lt;a href=&amp;quot;xxx&amp;quot;&amp;gt;thisn&amp;lt;/a&amp;gt; &lt;/span&gt;&lt;a href=&quot;https://discu.eu/q/https://github.com/marsupialtail/quokka/blob/master/blog/why.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://discu.eu/q/https://github.com/marsupialtail/quokka/blob/master/blog/why.md &lt;br /&gt;&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://discu.eu/q/https://github.com/marsupialtail/quokka/blob/master/blog/why.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#000000;&quot;&gt;and some black text&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
<widget class="QLabel" name="lblPicture">
<property name="geometry">
<rect>
<x>10</x>
<y>770</y>
<width>351</width>
<height>201</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
</widget>
<widget class="QTextEdit" name="txtHashtags">
<property name="geometry">
<rect>
<x>370</x>
<y>90</y>
<width>331</width>
<height>871</height>
</rect>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; color:#f6f5f4;&quot;&gt;#notthis&lt;br /&gt;#orthis&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot; color:#8ae234;&quot;&gt;#butthis&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot; color:#f6f5f4;&quot;&gt;#notthis&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot; color:#8ae234;&quot;&gt;#yes&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot; color:#8ae234;&quot;&gt;#yes&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot; color:#ffffff;&quot;&gt;#no&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
<widget class="QTextEdit" name="txtBoosted">
<property name="geometry">
<rect>
<x>10</x>
<y>0</y>
<width>361</width>
<height>29</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>29</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>29</height>
</size>
</property>
<property name="font">
<font>
<pointsize>13</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">color:rgb(154, 153, 150)</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; color:#5e5c64;&quot;&gt;Boosted by&lt;/span&gt;&lt;span style=&quot; color:#f6f5f4;&quot;&gt; Jon&lt;/span&gt; &lt;span style=&quot; color:#8ae234;&quot;&gt;Baker&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
<widget class="QLabel" name="lblAcct">
<property name="geometry">
<rect>
<x>10</x>
<y>60</y>
<width>361</width>
<height>29</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>29</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>29</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(119, 118, 123);</string>
</property>
<property name="text">
<string>@JonBaker@mastodon.xyz</string>
</property>
</widget>
<widget class="QTextEdit" name="txtUsername">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>361</width>
<height>29</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>29</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>29</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; color:#f6f5f4;&quot;&gt;Jon&lt;/span&gt; &lt;span style=&quot; color:#8ae234;&quot;&gt;Baker&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
<widget class="Line" name="line">
<property name="geometry">
<rect>
<x>366</x>
<y>90</y>
<width>2</width>
<height>871</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(119, 118, 123);</string>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
<widget class="QLabel" name="lblDebug">
<property name="geometry">
<rect>
<x>390</x>
<y>10</y>
<width>311</width>
<height>21</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);</string>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QWidget" name="">
<property name="geometry">
<rect>
<x>11</x>
<y>980</y>
<width>692</width>
<height>63</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="btnFirst">
<property name="minimumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/double-left.png</normaloff>:/buttons/double-left.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnPrev">
<property name="minimumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/icons8-prev-page-48.png</normaloff>:/buttons/icons8-prev-page-48.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnDislike">
<property name="minimumSize">
<size>
<width>106</width>
<height>61</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>106</width>
<height>61</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/red-cross.png</normaloff>:/buttons/red-cross.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnUnsure">
<property name="minimumSize">
<size>
<width>106</width>
<height>61</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>106</width>
<height>61</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/dont-know-woman.png</normaloff>:/buttons/dont-know-woman.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnLike">
<property name="minimumSize">
<size>
<width>106</width>
<height>61</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>106</width>
<height>61</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/green-tick.png</normaloff>:/buttons/green-tick.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnNext">
<property name="minimumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/icons8-next-page-48.png</normaloff>:/buttons/icons8-next-page-48.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnLast">
<property name="minimumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>61</width>
<height>61</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/double-right.png</normaloff>:/buttons/double-right.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>709</width>
<height>26</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources>
<include location="urma.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,281 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>533</height>
</rect>
</property>
<property name="windowTitle">
<string>Urma</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>9</x>
<y>9</y>
<width>93</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>Username:</string>
</property>
</widget>
<widget class="QLabel" name="lblUsername">
<property name="geometry">
<rect>
<x>140</x>
<y>9</y>
<width>16</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QPushButton" name="btnNext">
<property name="geometry">
<rect>
<x>731</x>
<y>9</y>
<width>60</width>
<height>54</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/icons8-next-page-48.png</normaloff>:/buttons/icons8-next-page-48.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>86</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>Hashtags:</string>
</property>
</widget>
<widget class="QTextEdit" name="txtHashtags">
<property name="geometry">
<rect>
<x>20</x>
<y>121</y>
<width>256</width>
<height>251</height>
</rect>
</property>
<property name="markdown">
<string/>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:8px; margin-bottom:8px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPrev">
<property name="geometry">
<rect>
<x>731</x>
<y>183</y>
<width>60</width>
<height>54</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/icons8-prev-page-48.png</normaloff>:/buttons/icons8-prev-page-48.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
<widget class="QLabel" name="lblBoosted">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>40</y>
<width>81</width>
<height>21</height>
</rect>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">color rgb(200, 0, 3);</string>
</property>
<property name="text">
<string>Boosted</string>
</property>
</widget>
<widget class="QLabel" name="label_3">
<property name="geometry">
<rect>
<x>10</x>
<y>358</y>
<width>281</width>
<height>113</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>72</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="lineWidth">
<number>2</number>
</property>
<property name="text">
<string>urma</string>
</property>
</widget>
<widget class="QWidget" name="">
<property name="geometry">
<rect>
<x>290</x>
<y>10</y>
<width>358</width>
<height>441</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="lblPicture">
<property name="minimumSize">
<size>
<width>0</width>
<height>160</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="3">
<widget class="QTextEdit" name="txtPost">
<property name="markdown">
<string/>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="btnDislike">
<property name="text">
<string>Dislike</string>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/red-cross.png</normaloff>:/buttons/red-cross.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="btnUnsure">
<property name="text">
<string>Not sure</string>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/dont-know-woman.png</normaloff>:/buttons/dont-know-woman.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnLike">
<property name="text">
<string>Like</string>
</property>
<property name="icon">
<iconset resource="urma.qrc">
<normaloff>:/buttons/green-tick.png</normaloff>:/buttons/green-tick.png</iconset>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>26</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources>
<include location="urma.qrc"/>
</resources>
<connections/>
</ui>

194
app/ui/main_window_ui.py Normal file
View File

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'app/ui/main_window.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(709, 1100)
MainWindow.setAutoFillBackground(False)
MainWindow.setStyleSheet("background-color: #232834;")
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.txtPost = QtWidgets.QTextEdit(self.centralwidget)
self.txtPost.setGeometry(QtCore.QRect(10, 90, 351, 671))
self.txtPost.setMinimumSize(QtCore.QSize(341, 181))
self.txtPost.setStyleSheet("background-color: rgb(154, 153, 150); border-radius: 10px; \n"
"")
self.txtPost.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.txtPost.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
self.txtPost.setObjectName("txtPost")
self.lblPicture = QtWidgets.QLabel(self.centralwidget)
self.lblPicture.setGeometry(QtCore.QRect(10, 770, 351, 201))
self.lblPicture.setMinimumSize(QtCore.QSize(0, 0))
self.lblPicture.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.lblPicture.setText("")
self.lblPicture.setScaledContents(False)
self.lblPicture.setObjectName("lblPicture")
self.txtHashtags = QtWidgets.QTextEdit(self.centralwidget)
self.txtHashtags.setGeometry(QtCore.QRect(370, 90, 331, 871))
self.txtHashtags.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.txtHashtags.setFrameShadow(QtWidgets.QFrame.Sunken)
self.txtHashtags.setObjectName("txtHashtags")
self.txtBoosted = QtWidgets.QTextEdit(self.centralwidget)
self.txtBoosted.setGeometry(QtCore.QRect(10, 0, 361, 29))
self.txtBoosted.setMinimumSize(QtCore.QSize(0, 29))
self.txtBoosted.setMaximumSize(QtCore.QSize(16777215, 29))
font = QtGui.QFont()
font.setPointSize(13)
self.txtBoosted.setFont(font)
self.txtBoosted.setStyleSheet("color:rgb(154, 153, 150)")
self.txtBoosted.setFrameShape(QtWidgets.QFrame.NoFrame)
self.txtBoosted.setFrameShadow(QtWidgets.QFrame.Plain)
self.txtBoosted.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.txtBoosted.setObjectName("txtBoosted")
self.lblAcct = QtWidgets.QLabel(self.centralwidget)
self.lblAcct.setGeometry(QtCore.QRect(10, 60, 361, 29))
self.lblAcct.setMinimumSize(QtCore.QSize(0, 29))
self.lblAcct.setMaximumSize(QtCore.QSize(16777215, 29))
self.lblAcct.setStyleSheet("color: rgb(119, 118, 123);")
self.lblAcct.setObjectName("lblAcct")
self.txtUsername = QtWidgets.QTextEdit(self.centralwidget)
self.txtUsername.setGeometry(QtCore.QRect(10, 30, 361, 29))
self.txtUsername.setMinimumSize(QtCore.QSize(0, 29))
self.txtUsername.setMaximumSize(QtCore.QSize(16777215, 29))
self.txtUsername.setFrameShape(QtWidgets.QFrame.NoFrame)
self.txtUsername.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.txtUsername.setObjectName("txtUsername")
self.line = QtWidgets.QFrame(self.centralwidget)
self.line.setGeometry(QtCore.QRect(366, 90, 2, 871))
self.line.setStyleSheet("background-color: rgb(119, 118, 123);")
self.line.setFrameShape(QtWidgets.QFrame.VLine)
self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
self.line.setObjectName("line")
self.lblDebug = QtWidgets.QLabel(self.centralwidget)
self.lblDebug.setGeometry(QtCore.QRect(390, 10, 311, 21))
self.lblDebug.setStyleSheet("color: rgb(255, 255, 255);")
self.lblDebug.setText("")
self.lblDebug.setObjectName("lblDebug")
self.widget = QtWidgets.QWidget(self.centralwidget)
self.widget.setGeometry(QtCore.QRect(11, 980, 692, 63))
self.widget.setObjectName("widget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.btnFirst = QtWidgets.QPushButton(self.widget)
self.btnFirst.setMinimumSize(QtCore.QSize(61, 61))
self.btnFirst.setMaximumSize(QtCore.QSize(61, 61))
self.btnFirst.setText("")
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/buttons/double-left.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnFirst.setIcon(icon)
self.btnFirst.setIconSize(QtCore.QSize(48, 48))
self.btnFirst.setObjectName("btnFirst")
self.horizontalLayout.addWidget(self.btnFirst)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.btnPrev = QtWidgets.QPushButton(self.widget)
self.btnPrev.setMinimumSize(QtCore.QSize(61, 61))
self.btnPrev.setMaximumSize(QtCore.QSize(61, 61))
self.btnPrev.setText("")
icon1 = QtGui.QIcon()
icon1.addPixmap(QtGui.QPixmap(":/buttons/icons8-prev-page-48.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnPrev.setIcon(icon1)
self.btnPrev.setIconSize(QtCore.QSize(48, 48))
self.btnPrev.setObjectName("btnPrev")
self.horizontalLayout.addWidget(self.btnPrev)
self.btnDislike = QtWidgets.QPushButton(self.widget)
self.btnDislike.setMinimumSize(QtCore.QSize(106, 61))
self.btnDislike.setMaximumSize(QtCore.QSize(106, 61))
self.btnDislike.setText("")
icon2 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/buttons/red-cross.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnDislike.setIcon(icon2)
self.btnDislike.setIconSize(QtCore.QSize(48, 48))
self.btnDislike.setObjectName("btnDislike")
self.horizontalLayout.addWidget(self.btnDislike)
self.btnUnsure = QtWidgets.QPushButton(self.widget)
self.btnUnsure.setMinimumSize(QtCore.QSize(106, 61))
self.btnUnsure.setMaximumSize(QtCore.QSize(106, 61))
self.btnUnsure.setText("")
icon3 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/buttons/dont-know-woman.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnUnsure.setIcon(icon3)
self.btnUnsure.setIconSize(QtCore.QSize(48, 48))
self.btnUnsure.setObjectName("btnUnsure")
self.horizontalLayout.addWidget(self.btnUnsure)
self.btnLike = QtWidgets.QPushButton(self.widget)
self.btnLike.setMinimumSize(QtCore.QSize(106, 61))
self.btnLike.setMaximumSize(QtCore.QSize(106, 61))
self.btnLike.setText("")
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/buttons/green-tick.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnLike.setIcon(icon4)
self.btnLike.setIconSize(QtCore.QSize(48, 48))
self.btnLike.setObjectName("btnLike")
self.horizontalLayout.addWidget(self.btnLike)
self.btnNext = QtWidgets.QPushButton(self.widget)
self.btnNext.setMinimumSize(QtCore.QSize(61, 61))
self.btnNext.setMaximumSize(QtCore.QSize(61, 61))
self.btnNext.setText("")
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/buttons/icons8-next-page-48.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnNext.setIcon(icon5)
self.btnNext.setIconSize(QtCore.QSize(48, 48))
self.btnNext.setObjectName("btnNext")
self.horizontalLayout.addWidget(self.btnNext)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem1)
self.btnLast = QtWidgets.QPushButton(self.widget)
self.btnLast.setMinimumSize(QtCore.QSize(61, 61))
self.btnLast.setMaximumSize(QtCore.QSize(61, 61))
self.btnLast.setText("")
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap(":/buttons/double-right.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.btnLast.setIcon(icon6)
self.btnLast.setIconSize(QtCore.QSize(48, 48))
self.btnLast.setObjectName("btnLast")
self.horizontalLayout.addWidget(self.btnLast)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 709, 26))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.txtPost.setHtml(_translate("MainWindow", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Sans\'; font-size:13pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" color:#ffffff;\">The magic of adulthood is in finding new and interesting ways of being disappointed </span><span style=\" color:#5e5c64;\">&lt;a href=&quot;xxx&quot;&gt;thisn&lt;/a&gt; </span><a href=\"https://discu.eu/q/https://github.com/marsupialtail/quokka/blob/master/blog/why.md\"><span style=\" text-decoration: underline; color:#0000ff;\">https://discu.eu/q/https://github.com/marsupialtail/quokka/blob/master/blog/why.md <br /></span></a><a href=\"https://discu.eu/q/https://github.com/marsupialtail/quokka/blob/master/blog/why.md\"><span style=\" text-decoration: underline; color:#000000;\">and some black text</span></a></p></body></html>"))
self.txtHashtags.setHtml(_translate("MainWindow", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Sans\'; font-size:13pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" color:#f6f5f4;\">#notthis<br />#orthis</span><br /><span style=\" color:#8ae234;\">#butthis</span><br /><span style=\" color:#f6f5f4;\">#notthis</span><br /><span style=\" color:#8ae234;\">#yes</span><br /><span style=\" color:#8ae234;\">#yes</span><br /><span style=\" color:#ffffff;\">#no</span></p></body></html>"))
self.txtBoosted.setHtml(_translate("MainWindow", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Sans\'; font-size:13pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" color:#5e5c64;\">Boosted by</span><span style=\" color:#f6f5f4;\"> Jon</span> <span style=\" color:#8ae234;\">Baker</span></p></body></html>"))
self.lblAcct.setText(_translate("MainWindow", "@JonBaker@mastodon.xyz"))
self.txtUsername.setHtml(_translate("MainWindow", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Sans\'; font-size:13pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" color:#f6f5f4;\">Jon</span> <span style=\" color:#8ae234;\">Baker</span></p></body></html>"))
import urma_rc

BIN
app/ui/red-cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

BIN
app/ui/thumbs-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
app/ui/thumbs-up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

11
app/ui/urma.qrc Normal file
View File

@ -0,0 +1,11 @@
<RCC>
<qresource prefix="buttons">
<file>double-left.png</file>
<file>double-right.png</file>
<file>icons8-next-page-48.png</file>
<file>icons8-prev-page-48.png</file>
<file>dont-know-woman.png</file>
<file>green-tick.png</file>
<file>red-cross.png</file>
</qresource>
</RCC>

View File

@ -1,21 +1,44 @@
#! /usr/bin/env python #! /usr/bin/env python
import argparse
import datetime
import ipdb
import os
import pickle import pickle
import random
import requests
import stackprinter
import subprocess
import sys
from config import Config from config import Config
from dbconfig import engine from dbconfig import (
engine,
Session,
scoped_session,
)
from helpers import (
index_ojects_by_parameter,
send_mail,
)
from log import log from log import log
from mastodon import Mastodon from mastodon import Mastodon
from models import ( from models import (
Accounts, Accounts,
Attachments,
Base, Base,
Hashtags, Hashtags,
Posts, Posts,
PostTags,
)
from sqlalchemy import (
func,
select,
) )
TESTDATA = "/home/kae/git/urma/hometl.pickle" from typing import List, Optional, Union
# TESTDATA = "/home/kae/git/urma/hometl.pickle"
#
# Mastodon.create_app( # Mastodon.create_app(
# 'urma', # 'urma',
# api_base_url='mastodon.org.uk', # api_base_url='mastodon.org.uk',
@ -27,32 +50,465 @@ TESTDATA = "/home/kae/git/urma/hometl.pickle"
# mastodon = Mastodon(client_id = 'urma_clientcred.secret',) # mastodon = Mastodon(client_id = 'urma_clientcred.secret',)
# mastodon.log_in('kae@midnighthax.com', '^ZUaiC8P6vLV49', # mastodon.log_in('kae@midnighthax.com', '^ZUaiC8P6vLV49',
# to_file='urma_usercred.secret') # to_file='urma_usercred.secret')
# hometl = Mastodon.timeline()
# hometl = mastodon.timeline()
# hometl
# len(hometl)
# hometl0=hometl[0]
# hometl0
# history
# mastodon.me()
# following=mastodon.account_following(kaeid)
# len(following)
# following[0]
# following[39]
# following._pagination_next
# following._pagination_prev
# history
Base.metadata.create_all(engine)
# mastodon = Mastodon(access_token=Config.ACCESS_TOKEN)
# Data for development class MastodonAPI:
with open(TESTDATA, "rb") as inp: def __init__(self, access_token: str) -> None:
hometl = pickle.load(inp) """
Initialise access to Mastodon
"""
post = Posts() self.mastodon = Mastodon(access_token=access_token)
import ipdb; ipdb.set_trace() self.me = self.mastodon.me()
# Parse timeline def get_account_following(self):
# for post in hometl: """
# post = Posts() Return a list of account_dicts that we are following
"""
page1 = self.mastodon.account_following(self.me.id)
return self.mastodon.fetch_remaining(page1)
def get_bookmarked(self, since: int) -> List[dict]:
"""
Return posts bookmarked since id 'since'
"""
results = []
data = self.mastodon.bookmarks()
while data:
# Add in new data
results.extend(data)
# Have we reached minimum id?
if min([a.id for a in data]) < since:
break
# Get more data
data = self.mastodon.fetch_next(data)
return results
def get_hashtag_following(self):
"""
Return a list of hashtag_dicts that we are following
"""
page1 = self.mastodon.tag_following(self.me.id)
return self.mastodon.fetch_remaining(page1)
def unbookmark(self, post_id: int) -> None:
"""
Remove bookmark on passed post ID
"""
log.debug(f"unbookmark({post_id=})")
_ = self.mastodon.status_unbookmark(post_id)
def update_database() -> None:
"""
Main loop
"""
mastapi = MastodonAPI(Config.ACCESS_TOKEN)
with Session() as session:
update_followed_accounts(session, mastapi)
update_followed_hashtags(session, mastapi)
get_and_process_favourited(session, mastapi)
get_and_process_bookmarked(session, mastapi)
def get_and_process_bookmarked(session, mastapi):
"""Get newly bookmarked posts and add to db"""
posts_fetched = 0
bookmarked = mastapi.mastodon.bookmarks()
while bookmarked and posts_fetched <= Config.MAX_POSTS_TO_FETCH:
posts_fetched += len(bookmarked)
if process_bookmarked_posts(session, bookmarked, mastapi.me.id):
return
bookmarked = mastapi.mastodon.fetch_next(bookmarked)
def get_and_process_favourited(session, mastapi):
"""Get newly favourited posts and add to db"""
posts_fetched = 0
favourited = mastapi.mastodon.favourites()
while favourited and posts_fetched <= Config.MAX_POSTS_TO_FETCH:
posts_fetched += len(favourited)
if process_favourited_posts(session, favourited, mastapi.me.id):
return
favourited = mastapi.mastodon.fetch_next(favourited)
def get_database_name():
"""Return database name as string"""
with Session() as session:
dbname = session.bind.engine.url.database
return dbname
def get_version_string():
"""Return Urma version as string"""
try:
return str(
subprocess.check_output(
['git', 'describe'], stderr=subprocess.STDOUT
)
).strip('\'b\\n')
except subprocess.CalledProcessError as exc_info:
gitproc = subprocess.Popen(['git', 'rev-parse', 'HEAD'],
stdout=subprocess.PIPE)
(stdout, _) = gitproc.communicate()
return stdout.strip()[:7].decode("utf-8")
def process_bookmarked_posts(session: Session,
posts: List[Posts], me_id: int) -> bool:
"""
Process bookmarked posts
Stop when we find post has already been marked bookmarked.
Return True if that's why we stopped, else False.
"""
for post in posts:
record = _process_post(session, post, me_id)
# Posts that are favourited and bookmarked are genuine bookmark
# posts: ignore.
if record.favourited:
continue
if record.bookmarked:
return True
else:
record.bookmarked = True
# TODO: mastapi.unbookmark(int(post.id))
return False
def process_favourited_posts(session: Session,
posts: List[Posts], me_id: int) -> bool:
"""
Process favourited posts.
Stop when we find post has already been marked favourited
Return True if that's why we stopped, else False.
"""
for post in posts:
if post.favourited:
record = _process_post(session, post, me_id)
if record.favourited:
return True
else:
record.favourited = True
else:
log.debug(
f"process_favourited_posts({post.id=}) not favourited"
)
return False
def _process_post(session: Session, post: Posts, me_id) -> Posts:
"""
Add passsed post to database
"""
log.debug(f"{post.id=} processing")
# Create account record if needed
log.debug(f"{post.id=} processing {post.account.id=}")
account_rec = Accounts.get_or_create(session, str(post.account.id))
if account_rec.username is None:
log.debug(f"{post.id=} populating new account {post.account.id=}")
account_rec.username = post.account.username
account_rec.acct = post.account.acct
account_rec.display_name = post.account.display_name
account_rec.bot = post.account.bot
account_rec.url = post.account.url
if post.reblog:
# We're only interesting the boosted post, not this onej
log.debug(f"{post.id=} {post.reblog.id=}")
boosted_record = _process_post(session, post.reblog, me_id)
# Record who bosed the post unless it was us
if post.account.id == me_id:
boosted_record.boosting_account_id = None
else:
boosted_record.boosting_account_id = account_rec.id
return boosted_record
rec = Posts.get_or_create(session, str(post.id))
if rec.account_id is not None:
# We already have this post
log.debug(f"{post.id=} already in db")
return rec
else:
rec.account_id = account_rec.id
# Create hashtag records as needed
for tag in post.tags:
log.debug(f"{post.id=} processing {tag.name=}")
hashtag = Hashtags.get_or_create(session, tag.name, tag.url)
rec.hashtags.append(hashtag)
rec.created_at = post.created_at
rec.uri = post.uri
return rec
def report():
"""Print report"""
print(f"Urma version: {get_version_string()}")
print(f"Database: {get_database_name()}")
print(f"Date: {datetime.datetime.now().strftime('%c')}")
print()
with Session() as session:
# Find the most popular hashtags that we don't follow
print("Hashtags you don't follow that feature in posts you like")
print("--------------------------------------------------------")
top_unfollowed_tags = (
session.execute(
select(Hashtags, func.count(Hashtags.name))
.join(PostTags).join(Posts)
.where(Posts.favourited == 1, Hashtags.followed == 0)
.group_by(Hashtags.name)
.order_by(func.count(Hashtags.name).desc())
.limit(Config.TOP_HASHTAGS_TO_REPORT))
.all()
)
# How many times was each hashtag in a post we didnt' like?
for (hashtag, like) in top_unfollowed_tags:
dislike = (
session.execute(
select(func.count(Posts.id))
.join(PostTags).join(Hashtags)
.where(Posts.favourited == 0, Hashtags.id == hashtag.id)
).scalars()
.all()[0]
)
print(
f"Hashtag {hashtag.name} {like=}, {dislike=} "
f"({like * 100 / (like + dislike):.2f}% liked)"
)
# Find the least popular hashtags that we do follow
print()
print("Hashtags you follow that feature in posts you don't like")
print("--------------------------------------------------------")
bottom_followed_tags = (
session.execute(
select(Hashtags, func.count(Hashtags.name))
.join(PostTags).join(Posts)
.where(Posts.favourited == 0, Hashtags.followed == 1)
.group_by(Hashtags.name)
.order_by(func.count(Hashtags.name).desc())
.limit(Config.TOP_HASHTAGS_TO_REPORT))
.all()
)
# How many times was each hashtag in a post we did like?
for (hashtag, dislike) in bottom_followed_tags:
like = (
session.execute(
select(func.count(Posts.id))
.join(PostTags).join(Hashtags)
.where(Posts.favourited == 1, Hashtags.id == hashtag.id)
).scalars()
.all()[0]
)
print(
f"Hashtag {hashtag.name} {like=}, {dislike=} "
f"({dislike * 100 / (like + dislike):.2f}% disliked)"
)
# Find the most popular users that we don't follow
print()
print("Users you don't follow that feature in posts you like")
print("-----------------------------------------------------")
top_unfollowed_users = (
session.execute(
select(Accounts, func.count(Accounts.username))
.join(Posts)
.where(Posts.favourited == 1, Accounts.followed == 0)
.group_by(Accounts.username)
.order_by(func.count(Accounts.username).desc())
.limit(Config.TOP_POSTS_TO_REPORT))
.all()
)
# How many times was each user in a post we didnt' like?
for (user, like) in top_unfollowed_users:
dislike = (
session.execute(
select(func.count(Posts.id))
.join(Accounts)
.where(Posts.favourited == 0, Accounts.id == user.id)
).scalars()
.all()[0]
)
print(
f"User {user.username} {like=}, {dislike=} "
f"({like * 100 / (like + dislike):.2f}% liked)"
)
# Find the most unpopular users that we do follow
print()
print("Users you follow that feature in posts you don't like")
print("-----------------------------------------------------")
bottom_followed_users = (
session.execute(
select(Accounts, func.count(Accounts.username))
.join(Posts)
.where(Posts.favourited == 0, Accounts.followed == 1)
.group_by(Accounts.username)
.order_by(func.count(Accounts.username).desc())
.limit(Config.TOP_POSTS_TO_REPORT))
.all()
)
# How many times was each user in a post we did like?
for (user, dislike) in bottom_followed_users:
like = (
session.execute(
select(func.count(Posts.id))
.join(Accounts)
.where(Posts.favourited == 1, Accounts.id == user.id)
).scalars()
.all()[0]
)
print(
f"User {user.username} {like=}, {dislike=} "
f"({dislike * 100 / (like + dislike):.2f}% disliked)"
)
def update_followed_accounts(session: Session, mastapi: MastodonAPI) -> None:
"""
Retrieve list of followed accounts and update accounts
in database to match
"""
mast_followed_accounts = mastapi.get_account_following()
mast_followed_accounts_d = index_ojects_by_parameter(
mast_followed_accounts, "username")
our_followed_accounts = Accounts.get_followed(session)
our_followed_accounts_d = index_ojects_by_parameter(
our_followed_accounts, "username")
# Add those we are missing
for username in (
set(mast_followed_accounts_d.keys()) -
set(our_followed_accounts_d.keys())
):
account = Accounts.get_or_create(
session, str(mast_followed_accounts_d[username].id)
)
account.followed = True
# Remove any we no longer follow
for username in (
set(our_followed_accounts_d.keys()) -
set(mast_followed_accounts_d.keys())
):
account = Accounts.get_or_create(
session, str(our_followed_accounts_d[username].account_id)
)
account.followed = False
def update_followed_hashtags(session: Session, mastapi: MastodonAPI) -> None:
"""
Retrieve list of followed hashtags and update hashtags
"""
mast_followed_hashtags = mastapi.get_hashtag_following()
mast_followed_hashtags_d = index_ojects_by_parameter(
mast_followed_hashtags, "name")
our_followed_hashtags = Hashtags.get_followed(session)
our_followed_hashtags_d = index_ojects_by_parameter(
our_followed_hashtags, "name")
# Add those we are missing
for name in (
set(mast_followed_hashtags_d.keys()) -
set(our_followed_hashtags_d.keys())
):
hashtag = Hashtags.get_or_create(
session, name, mast_followed_hashtags_d[name].url)
hashtag.followed = True
# Remove any we no longer follow
for name in (
set(our_followed_hashtags_d.keys()) -
set(mast_followed_hashtags_d.keys())
):
hashtag = Hashtags.get_or_create(
session, name, our_followed_hashtags_d[name].name)
hashtag.followed = False
if __name__ == "__main__":
"""
If command line arguments given, carry out requested function and
exit. Otherwise run full application.
"""
try:
Base.metadata.create_all(engine)
p = argparse.ArgumentParser()
# Only allow at most one option to be specified
group = p.add_mutually_exclusive_group()
group.add_argument('-u', '--update',
action="store_true", dest="update_database",
default=False, help="Update database from Mastodon")
group.add_argument('-r', '--report',
action="store_true", dest="report",
default=False, help="Report")
args = p.parse_args()
# Run as required
if args.update_database:
log.debug("Updating database")
update_database()
elif args.report:
log.debug("Report")
report()
else:
# For now, default to updating database
update_database()
except Exception as exc:
if os.environ["URMA_ENV"] != "DEVELOPMENT":
msg = stackprinter.format(exc)
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
"Exception from urma", msg)
print("\033[1;31;47mUnhandled exception starts")
stackprinter.show(style="darkbg")
print("Unhandled exception ends\033[1;37;40m")

1814
app/urma_rc.py Normal file

File diff suppressed because it is too large Load Diff

46
conftest.py Normal file
View File

@ -0,0 +1,46 @@
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
import pytest
import sys
sys.path.append("app")
import models # noqa E402 (import not at top of file)
from sqlalchemy import create_engine # noqa E402
from sqlalchemy.orm import ( # noqa E402
scoped_session,
Session,
sessionmaker,
)
@pytest.fixture(scope="session")
def engine():
return create_engine(
"mysql+mysqldb://dev_urma_testing:dev_urma_testing@"
"localhost/dev_urma_testing",
encoding='utf-8',
pool_pre_ping=True,
future=True
)
@pytest.fixture(scope="session")
def setup_database(engine):
"""
Made scope=function (the default) to ensure any committed objects
are removed
"""
from app.models import Base # noqa E402
Base.metadata.create_all(engine)
# seed_database()
yield
Base.metadata.drop_all(engine)
@pytest.fixture
def session(setup_database, engine):
session = scoped_session(sessionmaker(autoflush=False, bind=engine))
session.begin()
yield session
session.rollback()

Binary file not shown.

2
markdown_with_colour.txt Normal file
View File

@ -0,0 +1,2 @@
Some Markdown text with <span style="color:blue">some *blue* text</span>.

View File

@ -16,9 +16,9 @@ branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)} depends_on = ${repr(depends_on)}
def upgrade(): def upgrade() -> None:
${upgrades if upgrades else "pass"} ${upgrades if upgrades else "pass"}
def downgrade(): def downgrade() -> None:
${downgrades if downgrades else "pass"} ${downgrades if downgrades else "pass"}

View File

@ -1,34 +0,0 @@
"""Fixing table relationships
Revision ID: 0d4c8f368e00
Revises: 5281c8c8059d
Create Date: 2023-01-02 14:28:57.905087
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '0d4c8f368e00'
down_revision = '5281c8c8059d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('post_tags', sa.Column('post_id', sa.Integer(), nullable=False))
op.drop_constraint('post_tags_ibfk_2', 'post_tags', type_='foreignkey')
op.create_foreign_key(None, 'post_tags', 'posts', ['post_id'], ['id'])
op.drop_column('post_tags', 'posts_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('post_tags', sa.Column('posts_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
op.drop_constraint(None, 'post_tags', type_='foreignkey')
op.create_foreign_key('post_tags_ibfk_2', 'post_tags', 'posts', ['posts_id'], ['id'])
op.drop_column('post_tags', 'post_id')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""Fixing table relationships
Revision ID: 1132a20d56cb
Revises: 4a6731c9e71b
Create Date: 2023-01-02 13:13:07.084963
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1132a20d56cb'
down_revision = '4a6731c9e71b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""Fixing table relationships
Revision ID: 28448b5f994f
Revises: 1132a20d56cb
Create Date: 2023-01-02 13:15:19.817927
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '28448b5f994f'
down_revision = '1132a20d56cb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('attachments', 'followed')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('attachments', sa.Column('followed', mysql.TINYINT(display_width=1), autoincrement=False, nullable=False))
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""Fixing table relationships
Revision ID: 354c25d6adac
Revises: 0d4c8f368e00
Create Date: 2023-01-02 14:37:16.192979
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '354c25d6adac'
down_revision = '0d4c8f368e00'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,34 +0,0 @@
"""Fixing table relationships
Revision ID: 4a6731c9e71b
Revises: 563253042f7e
Create Date: 2023-01-02 13:12:21.344294
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '4a6731c9e71b'
down_revision = '563253042f7e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('posts', sa.Column('parent_id', sa.Integer(), nullable=True))
op.drop_constraint('posts_ibfk_2', 'posts', type_='foreignkey')
op.create_foreign_key(None, 'posts', 'posts', ['parent_id'], ['id'])
op.drop_column('posts', 'reblog_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('posts', sa.Column('reblog_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
op.drop_constraint(None, 'posts', type_='foreignkey')
op.create_foreign_key('posts_ibfk_2', 'posts', 'posts', ['reblog_id'], ['id'])
op.drop_column('posts', 'parent_id')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""Fixing table relationships
Revision ID: 5281c8c8059d
Revises: 28448b5f994f
Create Date: 2023-01-02 13:16:37.480999
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5281c8c8059d'
down_revision = '28448b5f994f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""Fixing table relationships
Revision ID: 563253042f7e
Revises: 7c67a545533e
Create Date: 2023-01-02 13:00:44.136697
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '563253042f7e'
down_revision = '7c67a545533e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""Fixing table relationships
Revision ID: 7672338beb90
Revises: 354c25d6adac
Create Date: 2023-01-02 14:39:26.283627
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7672338beb90'
down_revision = '354c25d6adac'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""Fixing table relationships
Revision ID: 7c67a545533e
Revises: 40a36c2e0e8d
Create Date: 2023-01-02 12:56:05.031475
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7c67a545533e'
down_revision = '40a36c2e0e8d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('accounts', sa.Column('followed', sa.Boolean(), nullable=False))
op.add_column('attachments', sa.Column('followed', sa.Boolean(), nullable=False))
op.add_column('hashtags', sa.Column('followed', sa.Boolean(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('hashtags', 'followed')
op.drop_column('attachments', 'followed')
op.drop_column('accounts', 'followed')
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""Initial Alembic configuration """Initial configuration
Revision ID: 40a36c2e0e8d Revision ID: 9abe92907482
Revises: Revises:
Create Date: 2023-01-02 08:15:56.863042 Create Date: 2023-01-06 20:44:30.249858
""" """
from alembic import op from alembic import op
@ -10,19 +10,19 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '40a36c2e0e8d' revision = '9abe92907482'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
pass pass
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
pass pass
# ### end Alembic commands ### # ### end Alembic commands ###

193
poetry.lock generated
View File

@ -1,6 +1,6 @@
[[package]] [[package]]
name = "alembic" name = "alembic"
version = "1.9.1" version = "1.9.2"
description = "A database migration tool for SQLAlchemy." description = "A database migration tool for SQLAlchemy."
category = "main" category = "main"
optional = false optional = false
@ -35,14 +35,6 @@ six = "*"
[package.extras] [package.extras]
test = ["astroid", "pytest"] test = ["astroid", "pytest"]
[[package]]
name = "atomicwrites"
version = "1.4.1"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "22.2.0" version = "22.2.0"
@ -88,14 +80,11 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "2.1.1" version = "3.0.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6.0" python-versions = "*"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]] [[package]]
name = "colorama" name = "colorama"
@ -113,6 +102,17 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "exceptiongroup"
version = "1.1.0"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
test = ["pytest (>=6)"]
[[package]] [[package]]
name = "executing" name = "executing"
version = "1.2.0" version = "1.2.0"
@ -124,6 +124,18 @@ python-versions = "*"
[package.extras] [package.extras]
tests = ["asttokens", "pytest", "littleutils", "rich"] tests = ["asttokens", "pytest", "littleutils", "rich"]
[[package]]
name = "fancycompleter"
version = "0.9.1"
description = "colorful TAB completion for Python prompt"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
pyreadline = {version = "*", markers = "platform_system == \"Windows\""}
pyrepl = ">=0.8.2"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "2.0.1" version = "2.0.1"
@ -144,6 +156,14 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "ipdb" name = "ipdb"
version = "0.13.11" version = "0.13.11"
@ -159,7 +179,7 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version <
[[package]] [[package]]
name = "ipython" name = "ipython"
version = "8.7.0" version = "8.8.0"
description = "IPython: Productive Interactive Computing" description = "IPython: Productive Interactive Computing"
category = "dev" category = "dev"
optional = false optional = false
@ -264,14 +284,6 @@ python-versions = ">=3.5"
[package.dependencies] [package.dependencies]
traitlets = "*" traitlets = "*"
[[package]]
name = "more-itertools"
version = "9.0.0"
description = "More routines for operating on iterables, beyond itertools"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "mysqlclient" name = "mysqlclient"
version = "2.1.1" version = "2.1.1"
@ -282,7 +294,7 @@ python-versions = ">=3.5"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "22.0" version = "23.0"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev" category = "dev"
optional = false optional = false
@ -300,6 +312,23 @@ python-versions = ">=3.6"
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["docopt", "pytest (<6.0.0)"] testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
name = "pdbpp"
version = "0.10.3"
description = "pdb++, a drop-in replacement for pdb"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
fancycompleter = ">=0.8"
pygments = "*"
wmctrl = "*"
[package.extras]
funcsigs = ["funcsigs"]
testing = ["funcsigs", "pytest"]
[[package]] [[package]]
name = "pexpect" name = "pexpect"
version = "4.8.0" version = "4.8.0"
@ -321,14 +350,15 @@ python-versions = "*"
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "0.13.1" version = "1.0.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=3.6"
[package.extras] [package.extras]
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "prompt-toolkit" name = "prompt-toolkit"
@ -360,14 +390,6 @@ python-versions = "*"
[package.extras] [package.extras]
tests = ["pytest"] tests = ["pytest"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.14.0" version = "2.14.0"
@ -379,27 +401,55 @@ python-versions = ">=3.6"
[package.extras] [package.extras]
plugins = ["importlib-metadata"] plugins = ["importlib-metadata"]
[[package]]
name = "pyreadline"
version = "2.1"
description = "A python implmementation of GNU readline."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pyrepl"
version = "0.9.0"
description = "A library for building flexible command line interfaces"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "5.4.3" version = "7.2.1"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0"
attrs = ">=17.4.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""} colorama = {version = "*", markers = "sys_platform == \"win32\""}
more-itertools = ">=4.0.0" exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*" packaging = "*"
pluggy = ">=0.12,<1.0" pluggy = ">=0.12,<2.0"
py = ">=1.5.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
wcwidth = "*"
[package.extras] [package.extras]
checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "pytest-env"
version = "0.8.1"
description = "py.test plugin that allows you to add environment variables."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
pytest = ">=7.1.3"
[package.extras]
test = ["coverage (>=6.5)", "pytest-mock (>=3.10)"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
@ -422,7 +472,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.28.1" version = "2.28.2"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
category = "main" category = "main"
optional = false optional = false
@ -430,7 +480,7 @@ python-versions = ">=3.7, <4"
[package.dependencies] [package.dependencies]
certifi = ">=2017.4.17" certifi = ">=2017.4.17"
charset-normalizer = ">=2,<3" charset-normalizer = ">=2,<4"
idna = ">=2.5,<4" idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27" urllib3 = ">=1.21.1,<1.27"
@ -448,7 +498,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "1.4.45" version = "1.4.46"
description = "Database Abstraction Library" description = "Database Abstraction Library"
category = "main" category = "main"
optional = false optional = false
@ -512,7 +562,7 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "traitlets" name = "traitlets"
version = "5.8.0" version = "5.8.1"
description = "Traitlets Python configuration system" description = "Traitlets Python configuration system"
category = "dev" category = "dev"
optional = false optional = false
@ -524,7 +574,7 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.26.13" version = "1.26.14"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main" category = "main"
optional = false optional = false
@ -543,16 +593,23 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "wmctrl"
version = "0.4"
description = "A tool to programmatically control windows inside X"
category = "dev"
optional = false
python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "56ce5fa8480bc5fc923ac1c8ae72ac995690fdfc1ba42cd62f9a8f4737919d8d" content-hash = "6595ea3da23f353d916879141650fbb0b213e1393466925fa7be5e2e5bb6d5a3"
[metadata.files] [metadata.files]
alembic = [] alembic = []
appnope = [] appnope = []
asttokens = [] asttokens = []
atomicwrites = []
attrs = [] attrs = []
backcall = [ backcall = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
@ -566,9 +623,15 @@ decorator = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
] ]
exceptiongroup = []
executing = [] executing = []
fancycompleter = [
{file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"},
{file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"},
]
greenlet = [] greenlet = []
idna = [] idna = []
iniconfig = []
ipdb = [] ipdb = []
ipython = [] ipython = []
jedi = [] jedi = []
@ -576,13 +639,16 @@ mako = []
markupsafe = [] markupsafe = []
"mastodon.py" = [] "mastodon.py" = []
matplotlib-inline = [] matplotlib-inline = []
more-itertools = []
mysqlclient = [] mysqlclient = []
packaging = [] packaging = []
parso = [ parso = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
] ]
pdbpp = [
{file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"},
{file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"},
]
pexpect = [ pexpect = [
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
@ -592,8 +658,8 @@ pickleshare = [
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
] ]
pluggy = [ pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
] ]
prompt-toolkit = [] prompt-toolkit = []
ptyprocess = [ ptyprocess = [
@ -604,15 +670,17 @@ pure-eval = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
] ]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pygments = [] pygments = []
pytest = [ pyreadline = [
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, {file = "pyreadline-2.1.win32.exe", hash = "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e"},
{file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"},
] ]
pyrepl = [
{file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"},
]
pytest = []
pytest-env = []
python-dateutil = [ python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
@ -636,3 +704,6 @@ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
] ]
wmctrl = [
{file = "wmctrl-0.4.tar.gz", hash = "sha256:66cbff72b0ca06a22ec3883ac3a4d7c41078bdae4fb7310f52951769b10e14e0"},
]

View File

@ -13,8 +13,9 @@ mysqlclient = "^2.1.1"
alembic = "^1.9.1" alembic = "^1.9.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^5.2"
ipdb = "^0.13.11" ipdb = "^0.13.11"
pytest-env = "^0.8.1"
pdbpp = "^0.10.3"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
@ -23,3 +24,11 @@ build-backend = "poetry.core.masonry.api"
[tool.mypy] [tool.mypy]
mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/urma-e3I_sS5U-py3.9:/home/kae/git/urma/app" mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/urma-e3I_sS5U-py3.9:/home/kae/git/urma/app"
plugins = "sqlalchemy.ext.mypy.plugin" plugins = "sqlalchemy.ext.mypy.plugin"
[tool.pytest.ini_options]
addopts = "-xls --pdb"
[tool.vulture]
exclude = ["migrations"]
paths = ["app"]
make_whitelist = true

BIN
tests/boosted.pickle Normal file

Binary file not shown.

BIN
tests/boosting.pickle Normal file

Binary file not shown.

BIN
tests/favourited.pickle Normal file

Binary file not shown.

View File

@ -1,5 +0,0 @@
from urma import __version__
def test_version():
assert __version__ == '0.1.0'

215
tests/test_models.py Normal file
View File

@ -0,0 +1,215 @@
from app.models import (
Accounts,
Hashtags,
Posts,
)
# Accounts
def test_accounts_creation(session):
"""Account creation"""
account_id = "109568725613662482"
acct = Accounts(session, account_id)
assert acct
assert acct.account_id == account_id
assert acct.username is None
def test_create_or_add_account(session):
"""Check we can retrieve existing account"""
account_id = "109568725613662482"
acct = Accounts.get_or_create(session, account_id)
acct2 = Accounts.get_or_create(session, account_id)
assert acct is acct2
def test_get_followed_accounts(session):
"""Test retrieval of followed accounts"""
account1_id = "109568725613662482"
account2_id = "109568725613662483"
acct1 = Accounts.get_or_create(session, account1_id)
acct2 = Accounts.get_or_create(session, account2_id)
acct2.followed = True
session.flush()
accts_followed = Accounts.get_followed(session)
assert acct1 not in accts_followed
assert acct2 in accts_followed
# Hashtags
def test_hashtags_access(session):
"""Test we can access hashtags table"""
result = Hashtags.get_all(session)
assert result == []
def test_create_hashtag(session):
"""Create a hashtag"""
h_name = "MyHashtag"
h_url = "https://example.com"
ht = Hashtags.get_or_create(session, h_name, h_url)
assert ht
assert ht.name == h_name
assert ht.url == h_url
def test_get_or_create_hashtag(session):
"""Check we can retrieve existing hashtag"""
h_name = "MyHashtag"
h_url = "https://example.com"
ht = Hashtags.get_or_create(session, h_name, h_url)
ht2 = Hashtags.get_or_create(session, h_name, h_url)
assert ht is ht2
def test_get_followed_hashtags(session):
"""Test retrieval of followed hashtags"""
ht1 = "HashTagOne"
ht1_url = "https://one.example.com"
ht2 = "HashTagTwo"
ht2_url = "https://two.example.com"
hashtag1 = Hashtags.get_or_create(session, ht1, ht1_url)
hashtag2 = Hashtags.get_or_create(session, ht2, ht2_url)
hashtag2.followed = True
session.flush()
hashtags_followed = Hashtags.get_followed(session)
assert hashtag1 not in hashtags_followed
assert hashtag2 in hashtags_followed
# Posts
def test_create_posts(session):
"""Test we can create posts"""
post_id = "109666763623624320"
post = Posts(session, post_id)
assert post.post_id == post_id
assert post.account_id is None
def test_get_by_post_id(session):
"""Retrieve by post ID"""
post1_id = "109666763623624320"
post2_id = "109666763623624321"
post1 = Posts(session, post1_id)
post2 = Posts(session, post2_id)
post = Posts.get_by_post_id(session, post1_id)
assert post is post1
assert post is not post2
def test_get_or_create_post(session):
"""Check we can retrieve existing post"""
post_id = "109666763623624576"
post = Posts.get_or_create(session, post_id)
post2 = Posts.get_or_create(session, post_id)
assert post is post2
def test_max_post_id_empty(session):
"""Test max_post_id with empty table"""
max_post_id = Posts.max_post_id(session)
assert max_post_id is None
def test_max_post_id_one(session):
"""Test max_post_id with one entry"""
post_id = "109666763623624576"
post = Posts(session, post_id)
max_post_id = Posts.max_post_id(session)
assert max_post_id == post_id
def test_max_post_id_three(session):
"""Test max_post_id with three entries"""
post1_id = "109666763623624576"
post2_id = "209666763623624576" # highest ID
post3_id = "109666763623624577"
post1 = Posts(session, post1_id)
post2 = Posts(session, post2_id)
post3 = Posts(session, post3_id)
max_post_id = Posts.max_post_id(session)
assert max_post_id == post2_id
# PostTags
def test_add_hashtag_to_post(session):
"""Test adding a hashtag to a post"""
ht1 = "HashTagOne"
ht1_url = "https://one.example.com"
ht2 = "HashTagTwo"
ht2_url = "https://two.example.com"
hashtag1 = Hashtags.get_or_create(session, ht1, ht1_url)
hashtag2 = Hashtags.get_or_create(session, ht2, ht2_url)
post1_id = "109666763623624576"
post2_id = "209666763623624576"
post3_id = "109666763623624577"
post1 = Posts(session, post1_id)
post2 = Posts(session, post2_id)
post3 = Posts(session, post3_id)
post1.hashtags.append(hashtag2)
assert len(post1.hashtags) == 1
assert post1.hashtags[0] is hashtag2
def test_add_two_hashtags_to_post(session):
"""Test adding two hashtags to a post"""
ht1 = "HashTagOne"
ht1_url = "https://one.example.com"
ht2 = "HashTagTwo"
ht2_url = "https://two.example.com"
hashtag1 = Hashtags.get_or_create(session, ht1, ht1_url)
hashtag2 = Hashtags.get_or_create(session, ht2, ht2_url)
post1_id = "109666763623624576"
post2_id = "209666763623624576"
post3_id = "109666763623624577"
post1 = Posts(session, post1_id)
post2 = Posts(session, post2_id)
post3 = Posts(session, post3_id)
post1.hashtags.append(hashtag2)
post1.hashtags.append(hashtag1)
assert len(post1.hashtags) == 2
assert hashtag1 in post1.hashtags
assert hashtag2 in post1.hashtags

View File

@ -0,0 +1,79 @@
import os
import pickle
from dateutil.tz import tzutc
from sqlalchemy import select
from models import (
Hashtags,
Posts,
)
from urma import (
process_bookmarked_posts,
process_favourited_posts,
)
BOOSTED_POST = "tests/boosted.pickle"
BOOSTING_POST = "tests/boosting.pickle"
FAVOURITED_POST = "tests/favourited.pickle"
ME_ID = 109568725613662482
def test_process_favourited_no_fave(session):
"""Test post not marked as favourite"""
with open(FAVOURITED_POST, "rb") as inp:
posts = pickle.load(inp)
# Sanity check
assert len(posts) == 1
# Set not favourited
posts[0]['favourited'] = False
process_favourited_posts(session, posts, ME_ID)
all_posts = session.execute(select(Posts)).scalars().all()
assert len(all_posts) == 0
def test_process_favourited_with_fave(session):
"""Test post marked as favourite"""
with open(FAVOURITED_POST, "rb") as inp:
posts = pickle.load(inp)
# Sanity check
assert len(posts) == 1
process_favourited_posts(session, posts, ME_ID)
all_posts = session.execute(select(Posts)).scalars().all()
assert len(all_posts) == 1
original_post = posts[0]
retrieved_post = all_posts[0]
assert original_post.id == int(retrieved_post.post_id)
assert original_post.account.id == int(retrieved_post.account.account_id)
assert original_post.created_at == retrieved_post.created_at
assert retrieved_post.favourited is True
def test_process_post_hashtags(session):
"""Test hashtags correctly parsed"""
with open(FAVOURITED_POST, "rb") as inp:
posts = pickle.load(inp)
# Sanity check
assert len(posts) == 1
process_favourited_posts(session, posts, ME_ID)
all_tags = Hashtags.get_all(session)
expected = ['fdroid', 'apps', 'android', 'foss', 'free', 'AndroidAppRain']
for hashtag in all_tags:
assert hashtag.name in expected