diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cf6e99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.mypy_cache/ +*.pyc +*.swp +tags +Session.vim +.direnv +.envrc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..696b650 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,85 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat migrations/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b794fd4 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.0' diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..a339023 --- /dev/null +++ b/app/config.py @@ -0,0 +1,21 @@ +import logging +import os + + +class Config(object): + ACCESS_TOKEN = '/home/kae/git/urma/app/urma_usercred.secret' + # KAEID = 109568725613662482 + # DEBUG_FUNCTIONS: List[Optional[str]] = [] + # DEBUG_MODULES: List[Optional[str]] = ['dbconfig'] + DISPLAY_SQL = True + # ERRORS_FROM = ['noreply@midnighthax.com'] + # ERRORS_TO = ['kae@midnighthax.com'] + LOG_LEVEL_STDERR = logging.ERROR + LOG_LEVEL_SYSLOG = logging.DEBUG + LOG_NAME = "urma" + # MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') + # MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25) + # MAIL_SERVER = os.environ.get('MAIL_SERVER') or + # "woodlands.midnighthax.com" + # MAIL_USERNAME = os.environ.get('MAIL_USERNAME') + # MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None diff --git a/app/dbconfig.py b/app/dbconfig.py new file mode 100644 index 0000000..df5c199 --- /dev/null +++ b/app/dbconfig.py @@ -0,0 +1,42 @@ +import inspect +import logging +import os +from config import Config +from contextlib import contextmanager +from sqlalchemy import create_engine +from sqlalchemy.orm import (sessionmaker, scoped_session) +from typing import Generator + +from log import log + +MYSQL_CONNECT = os.environ.get('URMA_DB') +if MYSQL_CONNECT is None: + raise ValueError("MYSQL_CONNECT is undefined") +else: + dbname = MYSQL_CONNECT.split('/')[-1] + log.debug(f"Database: {dbname}") + +engine = create_engine( + MYSQL_CONNECT, + encoding='utf-8', + echo=Config.DISPLAY_SQL, + pool_pre_ping=True, + future=True +) + + +@contextmanager +def Session() -> Generator[scoped_session, None, None]: + frame = inspect.stack()[2] + file = frame.filename + function = frame.function + lineno = frame.lineno + Session = scoped_session(sessionmaker(bind=engine, future=True)) + log.debug( + f"Session acquired, {file=}, {function=}, " + f"function{lineno=}, {Session=}" + ) + yield Session + log.debug(" Session released") + Session.commit() + Session.close() diff --git a/app/helpers.py b/app/helpers.py new file mode 100644 index 0000000..74f5bf9 --- /dev/null +++ b/app/helpers.py @@ -0,0 +1,54 @@ +import os +import smtplib +import ssl + +from email.message import EmailMessage + +from config import Config +from log import log + + +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) + + return button_reply == QMessageBox.Yes + + +def send_mail(to_addr, from_addr, subj, body): + # From https://docs.python.org/3/library/email.examples.html + + # Create a text/plain message + msg = EmailMessage() + msg.set_content(body) + + msg['Subject'] = subj + msg['From'] = from_addr + msg['To'] = to_addr + + # Send the message via SMTP server. + context = ssl.create_default_context() + try: + s = smtplib.SMTP(host=Config.MAIL_SERVER, port=Config.MAIL_PORT) + if Config.MAIL_USE_TLS: + s.starttls(context=context) + if Config.MAIL_USERNAME and Config.MAIL_PASSWORD: + s.login(Config.MAIL_USERNAME, Config.MAIL_PASSWORD) + s.send_message(msg) + except Exception as e: + print(e) + finally: + 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) diff --git a/app/log.py b/app/log.py new file mode 100644 index 0000000..fd0ed5f --- /dev/null +++ b/app/log.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 + +import logging +import logging.handlers +import os +import stackprinter # type: ignore +import sys +import traceback + +from config import Config + + +class LevelTagFilter(logging.Filter): + """Add leveltag""" + + def filter(self, record: logging.LogRecord): + # Extract the first character of the level name + record.leveltag = record.levelname[0] + + # We never actually filter messages out, just abuse filtering to add an + # extra field to the LogRecord + return True + + +class DebugStdoutFilter(logging.Filter): + """Filter debug messages sent to stdout""" + + def filter(self, record: logging.LogRecord): + # Exceptions are logged at ERROR level + if record.levelno in [logging.DEBUG, logging.ERROR]: + return True + if record.module in Config.DEBUG_MODULES: + return True + if record.funcName in Config.DEBUG_FUNCTIONS: + return True + return False + + +log = logging.getLogger(Config.LOG_NAME) +log.setLevel(logging.DEBUG) + +# stderr +stderr = logging.StreamHandler() +stderr.setLevel(Config.LOG_LEVEL_STDERR) + +# syslog +syslog = logging.handlers.SysLogHandler(address='/dev/log') +syslog.setLevel(Config.LOG_LEVEL_SYSLOG) + +# Filter +local_filter = LevelTagFilter() +debug_filter = DebugStdoutFilter() + +syslog.addFilter(local_filter) + +stderr.addFilter(local_filter) +stderr.addFilter(debug_filter) + +stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s', + datefmt='%H:%M:%S') +syslog_fmt = logging.Formatter( + '[%(name)s] %(module)s.%(funcName)s - %(leveltag)s: %(message)s' +) +stderr.setFormatter(stderr_fmt) +syslog.setFormatter(syslog_fmt) + +log.addHandler(stderr) +log.addHandler(syslog) + + +def log_uncaught_exceptions(_ex_cls, ex, tb): + + from helpers import send_mail + + print("\033[1;31;47m") + logging.critical(''.join(traceback.format_tb(tb))) + print("\033[1;37;40m") + print(stackprinter.format(ex, style="darkbg2", add_summary=True)) + if os.environ["MM_ENV"] != "DEVELOPMENT": + msg = stackprinter.format(ex) + send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, + "Exception from musicmuster", msg) + + +sys.excepthook = log_uncaught_exceptions diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..fc384f8 --- /dev/null +++ b/app/models.py @@ -0,0 +1,121 @@ +#!/usr/bin/python3 + +import os.path + +from dbconfig import Session, scoped_session + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + String, +) + +from sqlalchemy.ext.associationproxy import association_proxy + +from sqlalchemy.orm import ( + declarative_base, + relationship, +) +from config import Config +from log import log + +Base = declarative_base() + + +# Database classes +class Accounts(Base): + __tablename__ = 'accounts' + + id = Column(Integer, primary_key=True, autoincrement=True) + account_id = Column(Integer, index=True, nullable=False) + username = Column(String(256), index=True, nullable=False) + acct = Column(String(256), index=False, nullable=False) + display_name = Column(String(256), index=False, nullable=False) + bot = Column(Boolean, index=False, nullable=False, default=False) + url = Column(String(256), index=False) + followed = Column(Boolean, index=False, nullable=False, default=False) + posts = relationship("Posts", back_populates="account") + + def __repr__(self) -> str: + return ( + f"" + ) + + +class Attachments(Base): + __tablename__ = 'attachments' + + id = Column(Integer, primary_key=True, autoincrement=True) + media_id = Column(Integer, index=True, nullable=False) + 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: + return ( + f"" + ) + + +class Hashtags(Base): + __tablename__ = 'hashtags' + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(256), index=True, nullable=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") + + + def __repr__(self) -> str: + return ( + f", followed={self.followed}>" + ) + + +class Posts(Base): + __tablename__ = 'posts' + + id = Column(Integer, primary_key=True, autoincrement=True) + post_id = Column(Integer, index=True, nullable=False) + created_at = Column(DateTime, index=True, default=None) + 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")) + reblog = relationship("Posts") + + media_attachments_id = Column(Integer, ForeignKey('attachments.id'), + nullable=True) + media_attachments = relationship("Attachments", back_populates="posts") + + posttags = relationship("PostTags", back_populates="post") + rating = Column(Integer, index=True, default=None) + + def __repr__(self) -> str: + return f"" + + +class PostTags(Base): + __tablename__ = 'post_tags' + + id = Column(Integer, primary_key=True, autoincrement=True) + + post_id = Column(Integer, ForeignKey('posts.id'), nullable=False) + post = relationship(Posts, back_populates="posttags") + + hashtag_id = Column(Integer, ForeignKey('hashtags.id'), nullable=False) + hashtag = relationship("Hashtags", back_populates="posttags") diff --git a/app/urma.py b/app/urma.py new file mode 100755 index 0000000..38a67cb --- /dev/null +++ b/app/urma.py @@ -0,0 +1,58 @@ +#! /usr/bin/env python + +import pickle + +from config import Config +from dbconfig import engine +from log import log +from mastodon import Mastodon +from models import ( + Accounts, + Attachments, + Base, + Hashtags, + Posts, +) + +TESTDATA = "/home/kae/git/urma/hometl.pickle" + +# Mastodon.create_app( +# 'urma', +# api_base_url='mastodon.org.uk', +# to_file='urma_clientcred.secret' +# ) + +# API_BASE_URL = 'mastodon.org.uk' + +# mastodon = Mastodon(client_id = 'urma_clientcred.secret',) +# mastodon.log_in('kae@midnighthax.com', '^ZUaiC8P6vLV49', +# 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 +with open(TESTDATA, "rb") as inp: + hometl = pickle.load(inp) + +post = Posts() +import ipdb; ipdb.set_trace() + +# Parse timeline +# for post in hometl: +# post = Posts() diff --git a/app/urma_clientcred.secret b/app/urma_clientcred.secret new file mode 100644 index 0000000..51a26bb --- /dev/null +++ b/app/urma_clientcred.secret @@ -0,0 +1,4 @@ +0x02PA7g-THgRAXnB4bXJn5sml9aLybhOsJ-G0-_lgA +t5XSntLUVf8lrNLweZ5bC8KD6YoccD_G6-93ecgWEgg +https://mastodon.org.uk +kaemasto diff --git a/app/urma_usercred.secret b/app/urma_usercred.secret new file mode 100644 index 0000000..41b06f8 --- /dev/null +++ b/app/urma_usercred.secret @@ -0,0 +1,4 @@ +XAODnyOFfYEJ0OLXWkAM5QKkFknoyD8zjLaLPxmoECk +https://mastodon.org.uk +0x02PA7g-THgRAXnB4bXJn5sml9aLybhOsJ-G0-_lgA +t5XSntLUVf8lrNLweZ5bC8KD6YoccD_G6-93ecgWEgg diff --git a/hometl.pickle b/hometl.pickle new file mode 100644 index 0000000..6a34b79 Binary files /dev/null and b/hometl.pickle differ diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..d7da533 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,83 @@ +import os +import sys + +from logging.config import fileConfig + +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +path = os.path.dirname(os.path.dirname(__file__)) +sys.path.insert(0, path) +sys.path.insert(0, os.path.join(path, "app")) +from app.models import Base +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + +url = os.environ.get('URMA_DB') +print() +print(f"Alembic: {url=}") +print() + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = create_engine(url) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/0d4c8f368e00_fixing_table_relationships.py b/migrations/versions/0d4c8f368e00_fixing_table_relationships.py new file mode 100644 index 0000000..8008219 --- /dev/null +++ b/migrations/versions/0d4c8f368e00_fixing_table_relationships.py @@ -0,0 +1,34 @@ +"""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 ### diff --git a/migrations/versions/1132a20d56cb_fixing_table_relationships.py b/migrations/versions/1132a20d56cb_fixing_table_relationships.py new file mode 100644 index 0000000..ac8a43a --- /dev/null +++ b/migrations/versions/1132a20d56cb_fixing_table_relationships.py @@ -0,0 +1,28 @@ +"""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 ### diff --git a/migrations/versions/28448b5f994f_fixing_table_relationships.py b/migrations/versions/28448b5f994f_fixing_table_relationships.py new file mode 100644 index 0000000..d5eb89e --- /dev/null +++ b/migrations/versions/28448b5f994f_fixing_table_relationships.py @@ -0,0 +1,28 @@ +"""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 ### diff --git a/migrations/versions/354c25d6adac_fixing_table_relationships.py b/migrations/versions/354c25d6adac_fixing_table_relationships.py new file mode 100644 index 0000000..eb6582f --- /dev/null +++ b/migrations/versions/354c25d6adac_fixing_table_relationships.py @@ -0,0 +1,28 @@ +"""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 ### diff --git a/migrations/versions/40a36c2e0e8d_initial_alembic_configuration.py b/migrations/versions/40a36c2e0e8d_initial_alembic_configuration.py new file mode 100644 index 0000000..24d5988 --- /dev/null +++ b/migrations/versions/40a36c2e0e8d_initial_alembic_configuration.py @@ -0,0 +1,28 @@ +"""Initial Alembic configuration + +Revision ID: 40a36c2e0e8d +Revises: +Create Date: 2023-01-02 08:15:56.863042 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '40a36c2e0e8d' +down_revision = None +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 ### diff --git a/migrations/versions/4a6731c9e71b_fixing_table_relationships.py b/migrations/versions/4a6731c9e71b_fixing_table_relationships.py new file mode 100644 index 0000000..a4ee848 --- /dev/null +++ b/migrations/versions/4a6731c9e71b_fixing_table_relationships.py @@ -0,0 +1,34 @@ +"""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 ### diff --git a/migrations/versions/5281c8c8059d_fixing_table_relationships.py b/migrations/versions/5281c8c8059d_fixing_table_relationships.py new file mode 100644 index 0000000..5083caa --- /dev/null +++ b/migrations/versions/5281c8c8059d_fixing_table_relationships.py @@ -0,0 +1,28 @@ +"""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 ### diff --git a/migrations/versions/563253042f7e_fixing_table_relationships.py b/migrations/versions/563253042f7e_fixing_table_relationships.py new file mode 100644 index 0000000..4884e9b --- /dev/null +++ b/migrations/versions/563253042f7e_fixing_table_relationships.py @@ -0,0 +1,28 @@ +"""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 ### diff --git a/migrations/versions/7672338beb90_fixing_table_relationships.py b/migrations/versions/7672338beb90_fixing_table_relationships.py new file mode 100644 index 0000000..ad7a9d0 --- /dev/null +++ b/migrations/versions/7672338beb90_fixing_table_relationships.py @@ -0,0 +1,28 @@ +"""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 ### diff --git a/migrations/versions/7c67a545533e_fixing_table_relationships.py b/migrations/versions/7c67a545533e_fixing_table_relationships.py new file mode 100644 index 0000000..5ea41f1 --- /dev/null +++ b/migrations/versions/7c67a545533e_fixing_table_relationships.py @@ -0,0 +1,32 @@ +"""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 ### diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..225dbe1 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,638 @@ +[[package]] +name = "alembic" +version = "1.9.1" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[package.extras] +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]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs"] +docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["attrs", "zope.interface"] +tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] +tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "blurhash" +version = "1.1.4" +description = "Pure-Python implementation of the blurhash algorithm." +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["pillow", "numpy", "pytest"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +tests = ["asttokens", "pytest", "littleutils", "rich"] + +[[package]] +name = "greenlet" +version = "2.0.1" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil", "faulthandler"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ipdb" +version = "0.13.11" +description = "IPython-enabled pdb" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\" or python_version >= \"3.11\""} +ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\" and python_version < \"3.11\" or python_version >= \"3.11\""} +tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} + +[[package]] +name = "ipython" +version = "8.7.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.11,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "ipykernel", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "docrepr", "matplotlib", "stack-data", "pytest (<7)", "typing-extensions", "pytest (<7.1)", "pytest-asyncio", "testpath", "nbconvert", "nbformat", "ipywidgets", "notebook", "ipyparallel", "qtconsole", "curio", "matplotlib (!=3.2.0)", "numpy (>=1.20)", "pandas", "trio"] +black = ["black"] +doc = ["ipykernel", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "docrepr", "matplotlib", "stack-data", "pytest (<7)", "typing-extensions", "pytest (<7.1)", "pytest-asyncio", "testpath"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test_extra = ["pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.20)", "pandas", "trio"] + +[[package]] +name = "jedi" +version = "0.18.2" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx-rtd-theme (==0.4.3)", "sphinx (==1.8.5)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "mako" +version = "1.2.4" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "2.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mastodon.py" +version = "1.8.0" +description = "Python wrapper for the Mastodon API" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +blurhash = ">=1.1.4" +decorator = ">=4.0.0" +python-dateutil = "*" +python-magic = "*" +requests = ">=2.4.2" +six = "*" + +[package.extras] +blurhash = ["blurhash (>=1.1.4)"] +test = ["pytest", "pytest-runner", "pytest-cov", "vcrpy", "pytest-vcr", "pytest-mock", "requests-mock", "pytz", "http-ece (>=1.0.5)", "cryptography (>=1.6.0)", "blurhash (>=1.1.4)"] +webpush = ["http-ece (>=1.0.5)", "cryptography (>=1.6.0)"] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +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]] +name = "mysqlclient" +version = "2.1.1" +description = "Python interface to MySQL" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "packaging" +version = "22.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +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]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.extras] +checkqa-mypy = ["mypy (==v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sqlalchemy" +version = "1.4.45" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} + +[package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] +mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["pytest", "typeguard", "pygments", "littleutils", "cython"] + +[[package]] +name = "stackprinter" +version = "0.2.10" +description = "Debug-friendly stack traces, with variable values and semantic highlighting" +category = "main" +optional = false +python-versions = ">=3.4" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "traitlets" +version = "5.8.0" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] + +[[package]] +name = "urllib3" +version = "1.26.13" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "56ce5fa8480bc5fc923ac1c8ae72ac995690fdfc1ba42cd62f9a8f4737919d8d" + +[metadata.files] +alembic = [] +appnope = [] +asttokens = [] +atomicwrites = [] +attrs = [] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +blurhash = [] +certifi = [] +charset-normalizer = [] +colorama = [] +decorator = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] +executing = [] +greenlet = [] +idna = [] +ipdb = [] +ipython = [] +jedi = [] +mako = [] +markupsafe = [] +"mastodon.py" = [] +matplotlib-inline = [] +more-itertools = [] +mysqlclient = [] +packaging = [] +parso = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +prompt-toolkit = [] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +pure-eval = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {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 = [] +pytest = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +python-dateutil = [ + {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"}, +] +python-magic = [] +requests = [] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sqlalchemy = [] +stack-data = [] +stackprinter = [] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +traitlets = [] +urllib3 = [] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bc276a6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "urma" +version = "0.1.0" +description = "" +authors = ["Keith Edmunds "] + +[tool.poetry.dependencies] +python = "^3.9" +"Mastodon.py" = "^1.8.0" +stackprinter = "^0.2.10" +SQLAlchemy = "^1.4.45" +mysqlclient = "^2.1.1" +alembic = "^1.9.1" + +[tool.poetry.dev-dependencies] +pytest = "^5.2" +ipdb = "^0.13.11" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.mypy] +mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/urma-e3I_sS5U-py3.9:/home/kae/git/urma/app" +plugins = "sqlalchemy.ext.mypy.plugin" diff --git a/sample_reblog.txt b/sample_reblog.txt new file mode 100644 index 0000000..569bd27 --- /dev/null +++ b/sample_reblog.txt @@ -0,0 +1,135 @@ +{'id': 109615165821993543, + 'created_at': datetime.datetime(2023, 1, 1, 17, 38, 12, tzinfo=tzutc()), + 'in_reply_to_id': None, + 'in_reply_to_account_id': None, + 'sensitive': False, + 'spoiler_text': '', + 'visibility': 'public', + 'language': None, + 'uri': 'https://ohai.social/users/SecularJeffrey/statuses/109615165735453622/activity', + 'url': None, + 'replies_count': 0, + 'reblogs_count': 0, + 'favourites_count': 0, + 'edited_at': None, + 'favourited': False, + 'reblogged': False, + 'muted': False, + 'bookmarked': False, + 'content': '', + 'filtered': [], + 'reblog': {'id': 109615047364584011, + 'created_at': datetime.datetime(2023, 1, 1, 17, 8, 3, tzinfo=tzutc()), + 'in_reply_to_id': None, + 'in_reply_to_account_id': None, + 'sensitive': False, + 'spoiler_text': '', + 'visibility': 'public', + 'language': 'en', + 'uri': 'https://mastodon.scot/users/UndisScot/statuses/109615047228306787', + 'url': 'https://mastodon.scot/@UndisScot/109615047228306787', + 'replies_count': 1, + 'reblogs_count': 1, + 'favourites_count': 0, + 'edited_at': None, + 'favourited': False, + 'reblogged': False, + 'muted': False, + 'bookmarked': False, + 'content': '

A VIRTUAL TOUR OF SCOTLAND

We travel 22 miles north-west by road to Tobermory, the island capital of the Isle of Mull with a population just over 1,000 people. It faces south-east into the Sound of Mull, protected by Calve Island. Main Street hugs the harbour, with additional development on the hillside behind. More pics and info: undiscoveredscotland.co.uk/mul

Where to next? Vote via the poll attached as a reply.

#Scotland #IsleOfMull #Mull #Tobermory #Harbour #Argyll #UndiscoveredScotland

', + 'filtered': [], + 'reblog': None, + 'account': {'id': 109308679921192253, + 'username': 'UndisScot', + 'acct': 'UndisScot@mastodon.scot', + 'display_name': 'Undiscovered Scotland', + 'locked': False, + 'bot': False, + 'discoverable': True, + 'group': False, + 'created_at': datetime.datetime(2022, 11, 8, 0, 0, tzinfo=tzutc()), + 'note': '

Undiscovered Scotland is a combination of visitor guide, accommodation listing and business directory which aims to show you what the country is really like.

', + 'url': 'https://mastodon.scot/@UndisScot', + 'avatar': 'https://cdn.mastodon.org.uk/cache/accounts/avatars/109/308/679/921/192/253/original/c32d0bd7341746ec.jpg', + 'avatar_static': 'https://cdn.mastodon.org.uk/cache/accounts/avatars/109/308/679/921/192/253/original/c32d0bd7341746ec.jpg', + 'header': 'https://cdn.mastodon.org.uk/cache/accounts/headers/109/308/679/921/192/253/original/0d80ba57f8632e96.gif', + 'header_static': 'https://cdn.mastodon.org.uk/cache/accounts/headers/109/308/679/921/192/253/static/0d80ba57f8632e96.png', + 'followers_count': 3898, + 'following_count': 120, + 'statuses_count': 382, + 'last_status_at': datetime.datetime(2023, 1, 1, 0, 0), + 'emojis': [], + 'fields': [{'name': 'Website', + 'value': 'undiscoveredscotland.co.uk', + 'verified_at': '2022-12-08T08:28:32.376+00:00'}]}, + 'media_attachments': [{'id': 109615047281836119, + 'type': 'image', + 'url': 'https://cdn.mastodon.org.uk/cache/media_attachments/files/109/615/047/281/836/119/original/2216c7554c0b8ed5.jpg', + 'preview_url': 'https://cdn.mastodon.org.uk/cache/media_attachments/files/109/615/047/281/836/119/small/2216c7554c0b8ed5.jpg', + 'remote_url': 'https://media.mastodon.scot/mastodon-scot-public/media_attachments/files/109/615/043/452/921/824/original/82a752e9a05da694.jpg', + 'preview_remote_url': None, + 'text_url': None, + 'meta': {'focus': {'x': 0.0, 'y': 0.0}, + 'original': {'width': 1024, + 'height': 768, + 'size': '1024x768', + 'aspect': 1.3333333333333333}, + 'small': {'width': 554, + 'height': 416, + 'size': '554x416', + 'aspect': 1.3317307692307692}}, + 'description': 'Tobermory, the island capital of the Isle of Mull. The image shows a view down onto the harbour, looking along the length of Main Street. There are rooftops in the foreground and a row of buildings can be seen high on the hillside above the village, upper right in the picture. There is a twin masted sailing boat in the harbour in the left foreground.', + 'blurhash': 'UODTFiE3WFxbt-xvs.a~pKx]oJWX?wNHt7of'}], + 'mentions': [], + 'tags': [{'name': 'undiscoveredscotland', + 'url': 'https://mastodon.org.uk/tags/undiscoveredscotland'}, + {'name': 'argyll', 'url': 'https://mastodon.org.uk/tags/argyll'}, + {'name': 'harbour', 'url': 'https://mastodon.org.uk/tags/harbour'}, + {'name': 'tobermory', 'url': 'https://mastodon.org.uk/tags/tobermory'}, + {'name': 'mull', 'url': 'https://mastodon.org.uk/tags/mull'}, + {'name': 'IsleOfMull', 'url': 'https://mastodon.org.uk/tags/IsleOfMull'}, + {'name': 'scotland', 'url': 'https://mastodon.org.uk/tags/scotland'}], + 'emojis': [], + 'card': {'url': 'https://www.undiscoveredscotland.co.uk/mull/tobermory/index.html', + 'title': 'Tobermory Feature Page on Undiscovered Scotland', + 'description': 'Information about and images of Tobermory on Mull on Undiscovered Scotland.', + 'type': 'link', + 'author_name': '', + 'author_url': '', + 'provider_name': '', + 'provider_url': '', + 'html': '', + 'width': 0, + 'height': 0, + 'image': None, + 'embed_url': '', + 'blurhash': None}, + 'poll': None}, + 'account': {'id': 109392648174562172, + 'username': 'SecularJeffrey', + 'acct': 'SecularJeffrey@ohai.social', + 'display_name': 'JeffroTull', + 'locked': False, + 'bot': False, + 'discoverable': False, + 'group': False, + 'created_at': datetime.datetime(2022, 11, 11, 0, 0, tzinfo=tzutc()), + 'note': '

Semi retired. Living on colonized tribal land in the Pacific Northwest of North America. (((He/They/Him))).

On the cusp {born a late stage boomer, identify as early GenX}. Lapsed hippie.

Love is love. 🏳️\u200d🌈

#AltText
#DescriptiveTex
#DeadHead
#StarTrek
#StarWars
#SciFi
#Photography
#Nature
#Memes
#Birds
#Fossils
#Space
#Science
#Amphibians
#Reptiles
#Insects
#Invertebrates
#Archeology
#History
#AstroPhotography
#VintageCars

youtube.com/@JeffreyDuddles

instagram.com/p/CcycJtnvlqF/?i

', + 'url': 'https://ohai.social/@SecularJeffrey', + 'avatar': 'https://cdn.mastodon.org.uk/cache/accounts/avatars/109/392/648/174/562/172/original/cb6060134971c5f9.jpeg', + 'avatar_static': 'https://cdn.mastodon.org.uk/cache/accounts/avatars/109/392/648/174/562/172/original/cb6060134971c5f9.jpeg', + 'header': 'https://cdn.mastodon.org.uk/cache/accounts/headers/109/392/648/174/562/172/original/8b41057574e818ea.jpg', + 'header_static': 'https://cdn.mastodon.org.uk/cache/accounts/headers/109/392/648/174/562/172/original/8b41057574e818ea.jpg', + 'followers_count': 1766, + 'following_count': 4262, + 'statuses_count': 1615, + 'last_status_at': datetime.datetime(2023, 1, 1, 0, 0), + 'emojis': [], + 'fields': []}, + 'media_attachments': [], + 'mentions': [], + 'tags': [], + 'emojis': [], + 'card': None, + 'poll': None} + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_kaemasto.py b/tests/test_kaemasto.py new file mode 100644 index 0000000..9f918ec --- /dev/null +++ b/tests/test_kaemasto.py @@ -0,0 +1,5 @@ +from urma import __version__ + + +def test_version(): + assert __version__ == '0.1.0'