Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d40579e695 | ||
|
|
0c23956dd2 | ||
|
|
c7757efbf6 | ||
|
|
b15c1029ef | ||
|
|
66d9c7c6b5 | ||
|
|
e14177c069 | ||
|
|
d8f0beec43 | ||
|
|
b797746229 | ||
|
|
e6d8f10fe3 | ||
|
|
f034ef4f56 | ||
|
|
a25c6819ff | ||
|
|
aab8486549 | ||
|
|
a0da5bf218 | ||
|
|
c0ccea1dde |
1
.gitignore
vendored
@ -5,3 +5,4 @@ tags
|
|||||||
Session.vim
|
Session.vim
|
||||||
.direnv
|
.direnv
|
||||||
.envrc
|
.envrc
|
||||||
|
testdata/
|
||||||
|
|||||||
42
alembic.ini
@ -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]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
217
app/models.py
@ -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
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/ui/dont-know.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/ui/double-left.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
app/ui/double-right.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
app/ui/green-tick.png
Normal file
|
After Width: | Height: | Size: 389 B |
BIN
app/ui/icons8-next-page-48.png
Normal file
|
After Width: | Height: | Size: 595 B |
BIN
app/ui/icons8-prev-page-48.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
503
app/ui/main_window.ui
Normal 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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||||
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
|
p, li { white-space: pre-wrap; }
|
||||||
|
</style></head><body style=" font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;">
|
||||||
|
<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></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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||||
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
|
p, li { white-space: pre-wrap; }
|
||||||
|
</style></head><body style=" font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;">
|
||||||
|
<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></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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||||
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
|
p, li { white-space: pre-wrap; }
|
||||||
|
</style></head><body style=" font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;">
|
||||||
|
<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></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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||||
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
|
p, li { white-space: pre-wrap; }
|
||||||
|
</style></head><body style=" font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;">
|
||||||
|
<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></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>
|
||||||
281
app/ui/main_window_original.ui
Normal 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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||||
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
|
p, li { white-space: pre-wrap; }
|
||||||
|
</style></head><body style=" font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;">
|
||||||
|
<p style="-qt-paragraph-type:empty; margin-top:8px; margin-bottom:8px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html></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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||||
|
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||||
|
p, li { white-space: pre-wrap; }
|
||||||
|
</style></head><body style=" font-family:'Sans'; font-size:13pt; font-weight:400; font-style:normal;">
|
||||||
|
<p style="-qt-paragraph-type:empty; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html></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
@ -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;\"><a href="xxx">thisn</a> </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
|
After Width: | Height: | Size: 397 B |
BIN
app/ui/thumbs-down.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/ui/thumbs-up.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
11
app/ui/urma.qrc
Normal 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>
|
||||||
512
app/urma.py
@ -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
46
conftest.py
Normal 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()
|
||||||
BIN
hometl.pickle
2
markdown_with_colour.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Some Markdown text with <span style="color:blue">some *blue* text</span>.
|
||||||
|
|
||||||
@ -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"}
|
||||||
|
|||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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 ###
|
|
||||||
@ -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
@ -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"},
|
||||||
|
]
|
||||||
|
|||||||
@ -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
BIN
tests/boosting.pickle
Normal file
BIN
tests/favourited.pickle
Normal file
@ -1,5 +0,0 @@
|
|||||||
from urma import __version__
|
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
|
||||||
assert __version__ == '0.1.0'
|
|
||||||
215
tests/test_models.py
Normal 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
|
||||||
79
tests/test_processing_posts.py
Normal 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
|
||||||