From a301e345f4b8dcea7e576e8d006acbb8cd0fd621 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Mon, 2 Jan 2023 14:48:22 +0000 Subject: [PATCH] Initial commit --- .gitignore | 7 + README.rst | 0 alembic.ini | 85 +++ app/__init__.py | 1 + app/config.py | 21 + app/dbconfig.py | 42 ++ app/helpers.py | 54 ++ app/log.py | 85 +++ app/models.py | 121 ++++ app/urma.py | 58 ++ app/urma_clientcred.secret | 4 + app/urma_usercred.secret | 4 + hometl.pickle | Bin 0 -> 96252 bytes migrations/README | 1 + migrations/env.py | 83 +++ migrations/script.py.mako | 24 + ...0d4c8f368e00_fixing_table_relationships.py | 34 + ...1132a20d56cb_fixing_table_relationships.py | 28 + ...28448b5f994f_fixing_table_relationships.py | 28 + ...354c25d6adac_fixing_table_relationships.py | 28 + ...36c2e0e8d_initial_alembic_configuration.py | 28 + ...4a6731c9e71b_fixing_table_relationships.py | 34 + ...5281c8c8059d_fixing_table_relationships.py | 28 + ...563253042f7e_fixing_table_relationships.py | 28 + ...7672338beb90_fixing_table_relationships.py | 28 + ...7c67a545533e_fixing_table_relationships.py | 32 + poetry.lock | 638 ++++++++++++++++++ pyproject.toml | 25 + sample_reblog.txt | 135 ++++ tests/__init__.py | 0 tests/test_kaemasto.py | 5 + 31 files changed, 1689 insertions(+) create mode 100644 .gitignore create mode 100644 README.rst create mode 100644 alembic.ini create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/dbconfig.py create mode 100644 app/helpers.py create mode 100644 app/log.py create mode 100644 app/models.py create mode 100755 app/urma.py create mode 100644 app/urma_clientcred.secret create mode 100644 app/urma_usercred.secret create mode 100644 hometl.pickle create mode 100644 migrations/README create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/0d4c8f368e00_fixing_table_relationships.py create mode 100644 migrations/versions/1132a20d56cb_fixing_table_relationships.py create mode 100644 migrations/versions/28448b5f994f_fixing_table_relationships.py create mode 100644 migrations/versions/354c25d6adac_fixing_table_relationships.py create mode 100644 migrations/versions/40a36c2e0e8d_initial_alembic_configuration.py create mode 100644 migrations/versions/4a6731c9e71b_fixing_table_relationships.py create mode 100644 migrations/versions/5281c8c8059d_fixing_table_relationships.py create mode 100644 migrations/versions/563253042f7e_fixing_table_relationships.py create mode 100644 migrations/versions/7672338beb90_fixing_table_relationships.py create mode 100644 migrations/versions/7c67a545533e_fixing_table_relationships.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 sample_reblog.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_kaemasto.py 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 0000000000000000000000000000000000000000..6a34b79bd1b3ae9fc8961f91599ad0230680baf4 GIT binary patch literal 96252 zcmeIb3v?vcc^=3i=K;=e_>f3Slqk9ga>OACba(Z;N1P!r00uLhX$%Bl21C*mR9#(- zDxkZoSyc_7Mi14smZ9Y)Wmz%J3z}&)KG%5iLyhAqTJv8`YqI-Q{dO(ixG_?G z=>_3~#z*$m56gxkSxSCIvKsY~JbtyblEVM`xyIDtL;rr??RPz%1RIU|-PDL!2w7M0 zZ=-&|x>~hlyX%iO7SKW~uNX?DSX;656?8XOe~|w~l4ZSGw(M{2G?lWcS=xruxL&`@ ztms-%F;*;P3+$k?!yB5Z<(%OjtW9}(O1V+LqiSf4`UTapD&~oB zxH(a#E^AUTTs0NL3|DHFs+U*tnkiRJQ`5_4*t8@IUz%Ytl1Yl0WIPs&MpMyLBAH}F zi;eoJZbTMoKJ7C3gYph~8S21Bj7$Jnwv<&fq$Yk#>NdBSlh$DMh|xhEUy0d}V!x-oL&J~KM;^tFBU5xUaK5?$19X}^aW_5FAj7wBmFcM~mZ zxKY0gyS`GCYAZIqqxO9uEGx=-5i-WXqAuGv${jh~YJ8lvm-P+Bka9%@e;-&i^lGJX zT^&)6KvMX#eYkGupN4ublLq#eaqBd$d<9|*E|^Kf(&VCIjtO{4Yl>`{f>h26R@KrC zTrK!3r&p~$x1P;v5UNBZ$yh9r%H<1bDXJ);waRLv ze$;+`g(NwW3~yoA2USJND;Taez;Nwn`WS9fl9RDiA&EZXNiij3xC)KeCeR%3t?c_A zTb6???Z~p(3-{KY4{?yY0`9~0yIr~3ZTGd2`rVM~EXH=#Bwd4@70@c2v*IJ9Hyl(- z`kH34W3Zqp#XS9b+&=#5`|acD@LY4}LG##?KY}Ax!!+2c z*}I~VXmnhRjz^PAk@$&N}UmGls+owv`u9w{|TQnQN6DqZ=bkuf3!W6V)TRIjjy(3W} zuWTqqy+X@J$kha9D8V@gc|~xpF9E8@tY)2sf{fX)Y8CLlzo?bh!H`sixxui7D)nvk z!_MDTqu9WoNAV&yG+ZB!)OFA%18J=Jh0}7sYL(a{2R605r8csk-dD%fqphk|HxW@nZf_51m+lyV++B$z(x4~C_R7TyrUWYofofvHE;Noqa_)q@OBo+Yem z<~%|B9}9PW_A1O{j!a9|ug4B~GOha7IraWI^?~c^gX-JVx7S_6l==>4k5V7@_CoU{ zvCllm7B!Q{qG~fg#geh8n8;)zX_2;~=)Tfc8WZ;8j-TR)YcVhVG7t!!n>tXiH#uzi3DvpbD z0vVj3Eei_09hkiAk%x$0Sjx#_G=@g2x`i$=Y8bI}NdI&C=9oYPa9SADnF0>cHQHbf z3Wi=n^PAVc?9AJnuYcy6umGt}D8UleL3uFMb+clGN_tMS4QjafW8sJxHoO-#PU4OI zqQUS;S>L}yxrxp!0aa~PbS5qHX)2@-?Pmb7571v0iK67QC zeK=mw%d6POs-)q>QgP*s!5+gg%h%hCE-dj6#);Sg;cD;#9=>t zTC)T_2b;5r>uM7&nm%9cqK=z*Uf(3|%kG<9J?h+K2(UoI>thKoS4kd_2I8~|w@_e= zf;h(~Gw_kv^+<2AtD!zyzw9>YBy6{twjz-T#BBUTqn?PJmQ!**mKW27LNu3-MVUEE zoeYljRQ;u1T(_9zTsu`q-ZSxxn2#26g+g43$kB*xn^I>2?_oCk*LSODv+urLJ(sP# zMSZS=5s4I2b-rmff`UJCL6@1VN9K^jIN#wy9}_N}hf!h9Q-KeWLMXv#jz+)Qc9JMe zBD9p^trM(9PkEiX%XDx{i71|kB~OT%P&65FEhSA;(f-p=0%*YBXd7^=FgMik=sTr3 zXREsoI}K~}RBP9rT5-hOuOSh(nu(f&&t%%|WFzy2UIq?#g;!mz8>fZt4yMylU~j56t5g^c9~*QK2I?T3j3vYI zWIUXL3Z6>wYPeDmrC3~+(xN2C^OCKGA93|~QpD@U-S6KlNyTDV?AZBX12$SqVMc)Z zy4yD!k7wz3RNa2Y9m&MPF^oHgIcS>t6NPvtF2tM2r9zZ znXB)I)+IUisQQRY&NWcImVNkk^~bX}_tp2C+Ev&94khA|t#~?0o(r=A%R^)K*}wer z?}&f?ooKawfX#yU?0?yZXW!VT{w^Miq_^UUSVR5SX#baUpPYT;M764J)gPTKGp)Iz zS4h>A0A#@}4S7Y_RC1)2g&OL|U@SRv=JDqz7uKFVs~481)?U&TsrKs2R_fd(sb-}v zPe?Vr(6ljP*-h$4J`vQiOQhrBG^{^jQuL-jSxBZ7QI2KeaS_*)ZT(SyYqypf->1FQ zI(PWSZmcgckq$=^wS_n{(>7oYV1NIsbhv`zQ3~ zzrx;pJdFhtO*?P?=N|a@%2%uE@Acb~*2uylY!N^_sSt?t0+t4BZ%jHW;3&!qY?TO0 zkp9TqK`lrUtVj^>U~z!fPmWrMd{|ZX7tKXAwWzJVpkH|Pvc9@LlRLX*>d%e8N2+B% zpck?$($@O)+Kc?6`r1eC{A5szDk-MJQMmG=DQ;0kV!24FAW6x5JT6J`0^8HOx2X6& z?M2nO!(CZaQ5a-WVj`Tz;pLt(VJTA(Q<9WO$?2plC7l7@l0}v9FDhCYCM_R(HJz1; z_A;WAl9rKXNkGv#hY`1Ka(U^MV-b7ZEX=#>M!&$Xo74CHn@#y1|LGNt&l5Detw(~7$CLsLH|pb@$#8osd;=HBZ%jGnY1+}f4DdIv zeQsD2d~oJ>*uxv@RmWGReh^NSSMc*{{Q=%^ck|j8Ou<%BuHoAc;pa7Yzi=BX`_vED zcjWQH^^{0)>Uc^LV>wAm%Ss`oDAAa`3qOi3x0TqIC#5YWv0ZPflaSi}^Y5f8HNOYx z(G2fFx-l~TUk^QTl$zXsV??_0$V96RZO+J zl*D8vlT0MyaY!wX)FbZF%J*FW`7{_5|2=;!hCCH#C1KVQMmSMl@f?N&H$ zMX=*{s#Y?p&2c(b(%k^sGQ!*lw`XOUmUNXfM@N^*FW|T@$(DYFR?{gw=B(6&z18E zup=<{$$>aPI`!k{*tchCul@{vXdnN2XQ$YWkq>|EzvRC4PTV8m`kl?QoE&8LH~*UO zc6S5*vHR)du;4Z)r$2Ka;QD}{?BlI|vOha=w*%0#!MyO3-F29O2(Zv<<0|Z`<<&9a zw4rQi);MO3JQ`zAUM(D*<3&JRAW~9T3eW*YJ|vtgD-}yv#eqe^AY+2Lu2_JB;W3i2x2@YVvqVT33va@ZKu)WzvL;($yM31e-A40waUi6`X>SR>IL>(ireCItvLf z>%kxHy+y}+Qllp}RbAMWfD3~T#9CH?Jl1ih0#yLt3p)-%!ezy%3Ff9E;XmZO(96Xd zVUTFj5JH0yt)l~CsiNqWqGDqR@NEsF0N{XnVSh=uK7>V}UC&}*Z6;Rlm4xjB7g11G zmGZ^HOw~ZJOb{Do%HIXQ@U~@d5|I|OhQ4X3Q#$lVg6RaoVux;cHfecm%j%L|sLrsZ zxeQf?fF40`+TIJ#=(bGG!1%nXo-^(L2Em}ehk=+ojV)^j8wSCcM|~isd^b=KeEB*U zh_xwIQB36mMManw3t7ly6sQ>lkDYrOh}q{AGce)Kr9Bu!CF$bXS|Fmq^ET;$*s&B( zzTZW;7i0%w0~j;J!~}x=aYzru4)mTN)-%Ws#72o?sNl8?WL}!Y2Vx0f5$i>O`GS}p zm-avm!A%^D^vVcxtk@FiBlUOnG%FBdVz{`ROgIsZhhwm7q#`ibBqHH-f-~A=^0{Oo z9?7Lx$aX&MVkmB5XHx@>%tcke&eD5iIZ=`#h(;IXs92B-&d6qNWoNV5A3Xr9#d{6` zYw>f3dtxn^qnr%CPOOd!`&ZMT1*N2Ej3Er1&b94%@1VH_ucuIwtxO>y_H}~k=#hW6CohN&;<7@3v zIl#=6^mzMw=SO2V72lFIFANmU*pJgw??}H*Unf44sr?+uc(1s z^c;DPv2F&I+mC<$7oIcODRanqam>8p{64|6Pv(_FllI!^ggpMUBS%{;D*wbr9xovC z3Ibk)#br10NHGX_Jl~uOvXk`jQYW$dAv*>kjyR;bIqQof*ko4W&NE?W34$w*Jja={ z3(g@*1btInM@RHFzq8q%kTx_17Yu?^mo~>{UlWL&7eQGh9nynxbW61p%u&ZEkM?(G z$SRD@GMOI(d4C*z-;9x6%C>s;vqQP#CkQS%@*I+h$r1QxpdU{yNe~8Ye>hW#@kt^mmu|XvbXCI8y6h;Y{^pFqE zp8ucKv*&*e^7#3KJ>_v%3q{(4(h%^3v&%uE^o0^By7e_C&bo z=zrTb3%*`M6iSInrij7Pt;C1-;9|g$+fZi*-~zQOy!Oe5nBdGKoh$Bg-vQD&Duz=) zc%^|5jKnx}U_6(WW9hh@DnwI-M1l4*vRwckSlazgZJ1Y=9^de?c`*2L$ak)7i)hBU z(g1fQlF@J`o%GJDSULg`KO)AJxReG^&>5c(BbXEZs|rgY2R9hDnvGbVo(Ua$G4k9}^tqYP(!%=k)G=-LJOE%9o|WJC z)K+zIZEK@ZmD-_SN4V30bh+O74fw{MCU}HXBDSZ&H}0YpzPR=BY}Kz7zUv)>Xoa)x znr!Ojo4QfVJ6Ig3U{GDkie5Oa0Y=}d9*VI{ESXFrXgf}$$#kJL(L%=26Mawj(i7`Z z?OHewIiB zdYAtr&r-uCgjAvX{4H*N_)ATt=(-n<`DaI<0?|KC&M3H(HVMirGVMO*pR6r-^(Gq(dws+%TuJpz|>y zD^VK7ON*1XAH9H15N#w(sYM;ZZjcJ$1P8=Pzy$#-0XdBm;?qJ6k`z*4Evgdy$)OG+ zqvJse*(;0bC8v?f@u0@F!=lSm942ui@$ z0ncBXD$^pvu$$(rUMvN3VA+%hW(YaoX3DEU!3RC+12cA-M2#SpJR>|XI~NU@ag^+2 z5FEL*2WBcOLCRTBRsk53ia~JZ>tJB^E+891vA75?P9V}1M}A<|<{;3hOUjmH2F6}X z7X!1HWqH{Gxg#CY97|8TR>$^Cd$ye1qBZTr{20wZjR)8uiD0x0@{lHD2~V9*N||Iq zLfmpeiNOGA2Ph+X!?5PZAvzl#c+j)6d?p%A=ORf_R75EwCT&Q?@W6v1I-AXE1U$HJ z1cpwdx1qBMJRk=>%lu%rxPbrYk6(DFX(H3Pjik%o!vx5}1` zlt9P^X1jn_C6$@Pp=-9P`H^%oo{43|2$m1BN@yEYuR(oePVJ?%Bie{*;GQ8a{`@_^ zRra(0;9L7)-_1ieQ}Fb9N3nCp+CH@*YWmo+Dr02G&y^Ps7)rNBIuqGDONC!~_AGjy7Nk*m?R4+3qM| zl5G~Dbdx%e@A$hWkc-0Z4i*s_4eL*WwZd}!L}!q7h<&G?`HM zO2vH}eJ-xalJoL|SXWKr{E`}!u|-)s69|^Q$sOx~W6{2G+wFqJ7e4c+f3(Exf``5B zPd34KdVfx|Zep?s9-LWSMP`hS(I$r;AS0l6IzLl?17^%csZvo3c&T;@RTf`SF%X|@ zXxIbPhhoxtlTcd(sY?Xd10~q>5M9C+nZ5C<2G*=lK=6lTB2ex!(n|tuVeb}G)d~O! z;14WBYg4C{ge6NQSBN<60&FaIUrbJ)od!)*z(JZzc}%!Cg+E_Ll#1bQ{#6Pf!Y-Mq zg=b2x%+ky)+&U>??)=<|#1e}n!Z>)5MkX1K#UtTNiigCxgVrtGKT~?Vk9ZMPS!zWG1p;BxXd9so6Vls2Z`nWeUMl_&|6~h zT(fuF{K?OgkG0*qNZ!s|wN`>|sl#ZmFu$V0cFisJu6E(VJs)^iY(IY0X1=N4Yd`of zW)sGcvSbf5IA3aaUvgXj1C5gh4`zl8 z#EQ5@nH?~ArK)MR8grpL?F=J@5?Qfk~S zm7H4yS6o3O@w~lM2M@1pY43Sg?`(cUMB4T0jzpRXVHWFmJA7=~68ZL=w6yp8Xq%Z{ zZ;X6*^SNjK<`=IKviq>7XE6c8`?I5fab#9fg$o2#Vp2w1?$__!^+=19aplV>LjghK zoGsBhEVi3+oi98!km)3W31zLa7?bSVH%6;&%9wHloggGxhqSxW$z@1^N&uTf;i);n zy%3O~S4dEUsHV?xKnWDefeLvIP4fEclXe5B42Z^Mk{aO~#~^CKM=C22)kuw*g69@# z?*#G!&ngh^$}Aw4-h<+5P|!@sHjLP9re0JGQnzIU-a?tek%jcD(2OXS7#VHYX#p2_ z1s|IX$i{MhQxr7mWJsYvjmdit9-%n7X|{BVca=?>9vK2aoBX;6BW9_;oj_p9R4z;{ z+d?JEy1<$@ACy zg!moGB1+xKVqsV+Z+xj)&%9%7BtbBG`Xun=%rK;A=8h5$g_!ojqR+Wf50@t7E8wJ(~9 zihTGx7gy}oYxO zQ43bX0oT?v%AVF(LMlhmZ(BM(X4yT==mi(Y!jX5^0aZrmm_7f-$X*=FDW}-yUL4EL zily$wvB1{YwT`i~$l9aq_u^OpiUofEFrpL?#gc{@fXlZLjYtK=>A-!IDr6F3${w9Bh-J-0 ze7dcRtyS@BGlXSrga3<5{y{%nVl(_5-7tB$uhzTsCb8JgUF_g zC;`~9sMQ?0t-`Q$hE8n+4Zr~Ad{e?-!fDCWkd<2^Pdhb7*<%$3B1vAS^4jIRlZ074 zqb-Zd_`KSW-aPh*PbsVFF(8XlJttCaFIAorFaK{I8sL)adva{?^gUe z;@j^ZL`U375KRI(voo1!Di%*f$l^R$5RL!oo?4>_t#ML-Z4Pc`1qMA8p&=CwerH$} zS=!~CLiO(eY=ke_vCp;p%J*Vu_F`x{!L0XUX!?yda^qZraA&x*ISr%T^V8||xp6K* zFy+!7n5n%Onyy6vZHTb#vfGQHA*(wKh5hb8JJKbHrM?$KgOKY%tRj0cH1LyR#a8rd`WV=sn=<%kYB2&*pjR|%2Y za~eAb?Cfd`P4Tub|7vRx%Dud~ongpvpny=xU{fd{)S19R02{53A(=#} zq?eyCkt2c`kYt=8QZ8g?gjiInW#K%~;wY>IlMq>jP(a2K<}~D_(12(%nTeX)XNrDnkOv3w|Q_!B$Rls0$230f|IoAzhcP@-mU34$S;^1uwa8zu;rT-pOOMaS6^9TtJata;Q2W-J;Bgd>n0m=)(h4C1{x zqz7gRX?=oV2V^-`x}kNN_h=XiW$hc0;Li^_zfbtDP71Ah+oUjSXjiWmaf_TLSTHcL zv^JV4mp;y?Tsnj+oV&%Jx$+mN^@C%Po(v(ckpW0AVgyf9`5ho{YfdhtF+gMC#wb-~ z6}aDOp%4nyRt?5qGR;irBm_SJzSPCXs^@X)6c$D%8QY0r25k~S}N&*E2Y??s( zIyq#`6aAgi$UquMRtTcYt(^XtLBV0l=ZkDE6oi%oxzcn6sb~6?@bNW(@ItaNZ$pTOcVuJn928 zMiCJ~tbd|AFf#y72bratBR?=}6muKIJQ3Z2nPEXIh|Ev|$yVl3ADFQPWlh)0Gnayx zDVO%ZOr1GBKNAE)M0Xfw76M{sVPIxvmzFLD!3@zIm>KB@Q{Krh#M*@rB`(Q&5c9*+ zAAFuDq%El8hb&%>h!4!x8Le0fVy=kpz|6z~IjduXpVYT5H1mhimNf5#H3qS6T-pP# zozp?3++>6~c5Jo#k@}BxF-5ypWD2Cz+&0})pLcYC#6%HE#1KfCjv-_T={fl7W$9c> zp|llBK9y8*`8d#&l=FP@mi9JmC?%lgJ;#r(xJC8*pmYalxyG8svT{3VgmA5M5=v071#3i1dV z$liXb{>a%H{$|PC$#ei^EhJ3YN;nis06CZWS_8|dwoyh{Ce`Y~vrfKjq^QXP^(mF9 zf-tgK=c|M#wV(Be@NpSMV09xnq~NYr_i}Z8Ow6E7jlB5(LuK0yLC-Za)O38#ZRkCF zN7Y1r`0e_mM&2_ASS~7hs&_pwyP5agK^SJ>W1$Bh>p4899rvWFA&#W7{gX~R@5vF# z@x9Qq$9neKZ0S9Xv`CPtn9Y}-f1>B}Tw6CIf`i@VGCTC)nQ5pS_2?9vs%R1bRL>DvGgD!8GKP%{9_Wg7DkT94s4s@*h8#>B*&5=rJz z<3>)W3pv?7K|hMu^s)Nmm!iq1qUS5mMIL|OQ{!XPkH__Ml~nZDg~y+|`24ww{@AhX z)(hBlzG8Scf8(WJE&37L_xF_v(T(ce5n>x9IwP@2CW@k#v>OIQZ2O+>iP%2P5Zh+0 zte0y}ves3;cqAl!Rp5C(!p?m@Ww1(3Qh z>mZa3mo_KecXy5OB{f`_br4Lsv|E~}^)(tuM_y4bJ#RESJ1b(=F)W;U#2Osu* zGU1;QLJ=N)9`k{^h&C9+#twd5P5H2c4>IUrvx>$u_3wdw0_y2yq)2n;66p&JDwFU; zUO;&SLbIC+GX0}a0R%b4aA1PKyFbH&w$*X6j={#(RSj^K;6wpvqlyQ#tBzNVqCk)` zcun5>@UXnfu_K4S(+T99d3@C*qk@Djkofqe)5HvEQiI$U1j6oBheV zQD&P`a2{=Xjd0-5KaGzx7VJ7R^Qbp-;K>g)j;o9HgNurR91-NcY0q2j?geZGlQjzg z&i{j`7Tuc>cd_#uv1>|qmW(qxF5=(xl9)OXi=9ZML&-$iP3tXnEzHAZc>dsj{J<}b z*r(Lp{(FRusdxB4n&9ppI;0M>D@R{~LBEM^_7#*p;=D2S-jC8dd*;gYJq+EvsFXC| zdRhHg23IERFhY3{vx6G4SE=%fq2-X_g{sZD$ynwkO0z8sOibKV02l@onf)P5B26#S zwuFS^$B&;?!Z-tK;j>!l$tRygSy$v?#JPCU&gg>L&K2PUa7iGyF{s0#i0g6{nTJ`b z-!TE@S#WX{kYdCHLfODMI;)hgyh6^$3b1Af3aV5zxC0%k7g+vb6k)-CL&BS{f9k9M z=~q5=^V%ohe7*j0r%Brh%1xpTwV5nhs4o@>1lJ+$E-V&lj%1qK^E9o%MWa;+vJBpW zkoNsOa2!&$&wdz`@@G*$Cm7aj+5bhO!<6Q!Z_eDWSiqq9>Pe@&&~e(H?@U zpvvg9W^C_olDH(ULCh0Jo|~%vSes)`)gZXCDG$t0RtcywDI+{EJEt}Haf6s1Mwnx# z&;52rM=9Q7ARIZQIiC7kLW>pnTZ5P)MtE>`Rj1Gk+(p2a!3!s66AkN$wA8vX0pzexehq?`h1U* zKvXM~=RA;L6Oa1foSj7~JwWY(SYQrmj;H?C*Q99~I(KOX!CPw|12cJ9LlVhVWhx-q ziX+dl=BgfTA~GMDKj9byr>prqJ359)tm3l=acZb*dvkf>wN-z9fEF$~J;2d*zwVJ6 zfDmXX^odCaY>k!BsD!8Fn*5wq-YC9&Cj6|niZX)vliBI#N^@5)uV>euKRJ{QYP+!$ zBQC%E_`N&{z1i&aEH@SHqi`Q^CWnlmO$a8hP)_(Y_8sA-$!rr(hs9JJS!kp9FPDW@ z&Y>oAM2x{sge1F#%+7JsL-e*o`&YNIrE_uM#8Zh>TuF-E@s%(%41u`4&4~}tM3WtTm25l$dKlkWxDzuT+rbKcgr_1$v*!>rDmymc_puz zu&z)_7PG~EpTT6tkTNEdh{aO0biLXnaM|eyWp{l66CG$d31oaEdR8<3#vRPaw2VZt zRI>^NLQw?^(dV!+%`;ZJS!V2Lb(@8dSc+=Yo&2UY@87rw;Kj>zPQ`H z^V!my&rZ*BXG@o1xKDYf@3i+mJ4D>Gw;fWd;g`eB)xm=O|J$Fw-mJS5!B0ImeORh} zE)18DH=}J=cI$p(u>s6RES0F9bP!SxN1o%b2UO45>o#v}{{+I8vomShSxQXWqz$_i z`<%*ItZ&?BMkk(TH3{EhuQmSUA*=Oh`^U{Z9nL|+Tx|2KOJr+O){BTUNuWq!i;Ha^ zIXJnK@|i1NffItz%~XXOg4Gzg+15xCvJ;6{sRkb<*^qL`-qh!$+I$2tsWxTqH8bZK z__xPQS+vX|sxT==uN}V0CMayr>Mc9&Mo%^OsgD77G+p+m`!y#hwO8eIeV6u-{Ojjg zFKvJK5P}YYW)PGRK9Gr{_H$Yc!`&Vh<6KdKXgZOM#S*DpzL1ups3**d5)A7u9ICUa zfr8q+a?IOwmXzdVELBLt7#~lHDGHLH5W1OL+S_dQ!o6@9J`8#3Ya>mMAu5Wp?aO-% zy-=-|a<_lbc06}pfImb#08wt9n=qPOio{REA}3;E2!IgRP1y9H9=^!X0Xf}bL2zI1 zJ{|V<`9aKs-Lt#Lw?6{9Oh7CoW|~C{yG8OH;ciM2T8}s799L-N>+<^@ms|+>idkyg)M9C zrS+-H6MA8*y6}`W|;L=dGN8&{dV%8H6j5jwhgs zrV|MFrOLV+2w}yVnp1m)VPRfb84PEGLlOMhzuEX-->dTwt_NabKis>D;I#YJ?HY>} z#8%eabfIOZ0Pxu=@|3kGfJreKi^O7LTueuq0+{SXXkyY&gDHM3j|%7C0{GV3JHxjq z?hL+vlo2i?Q`!a!!Bi_1R1JZBj<5n^A`(YHCKjV0lU)qOlBu|hg#biBfoZp!#QC^h zexL9OhUB{2&mzM}_&=K9riP@5r4l|zU1I_;U|{3;2uQDWN}+}zW`sps1g1cH z`i2^pfvk|LhD4Q@=_^FwsiY(;MFi>P6czd8fNaoEwAtAHo~&6?gW_)>zmQVj)^ExVL!%e&@h?Uvmup0TbsciUVyWuAGrEhm09?R4DA z2)B%XM<_L*)1qXbi^RgwIBa#mc{WjM@kl|8XXIQVl}YFGne@&IMFSEycxS_ko!fL4 zl_Tjw9yN#}INbATH*>?VV&`zZ&1V1cL$K4m4M4T*&m8D3cQRXC{Z5ppGAzow*r<=V z=K#qDeFQDTv}Wn06{V!FXaw$1ph^8X!mDdkr`$K14 z0Xnl$f1i8&cGqyW?a!tc?%dmbNH(eATv^e|D-}&%2QcsBRY~Omd-q<%S%nnMfM6|a z#XM=?mvChvErSDGYuYbg{N^XV&91FoEg+W!tP>jswHi}#BnXLd&`v2T#}auFrI0g8 zDOzw!tnESpx%NKo0&<-@e4~y)e5+!f2!~0kGDG|U!b-}m;POJy520;MyA2g(4&%-Y zi-~kNk`%)+vfkrVXr2hML?MlB6N%)LVk9LxxK&^MIQ3sSGQ(@xRjT%e|NGtgp`@7D z!Vjwp_ojVI@pa+;1QJ{p|A&_GbYu%ZzLr(Unz5x#on6kjc?s1gA?_RbK`H-$g*>LJ zrWErg?q@^aq@xRG%OV zdU6lzsG=;7m$#*D!3T`1;K*|?us__#9@eq3^%>9eZEHzjTpq6^r&<1vQN(Tu#SdQQTe>&{cQpc zj2Of#<;eTqsU5u4=8D?GI{yC%>-c$d!!|eUfga&k-vd3uinu-K@!t5{mkWMR-O-0Q z$4XaEo$KprrYms$wN3>vyE-0149t}hY;Tkiw#D0(NhFYTAd-rTNViDl-DbLi7E*}& z=czbAN8iIefsP*pJP9tZnCrkrP(RR=Qf-V9u)m zyBj(&cBDxe>5^A)L{er&P#c z04Lxuj-}b_#E|AFAIGAlZ$9nRS@ULKsXIe1-55`x{4pV~s*N*U2hD0zfePaf(`i50(sbqqvIbdG9S1SuRx zf?h*Gb5MvzqOnJXn_v7{1f6%nG{bR_(^e7MzOEODU*>w59h>7#456+pa>d2k}Z)y;XHHmY4_4hJK1}Olt6a-f;ZJw!;1YdKX3$0WQZV!o{iqeIN(9o}gcC zQ`&w@@4AFQ2nLtu-4hYqK-hUhA4iFYGJ-Ks9$Xv8GOq!@N&o{odF)TA^Z>*%mhT7U zQXg$fWR$t5eNsB<5V90ZygSi#X9toG1Uvc%BM7ypI&0y}uR!huL>A46;WY3t$rylS z@n|@TatKYxh?tjw7b!%eaweL~0Fm+NsRsz>+NRc1_OsvVV~2--pj*7-S9`7$TDe6CPPKzU*TlYG_`vL!etCq)gnf(9V^a(hmJ(xIu_(-8)bbzv{>ek54^#;g zRzeal3#F|qWSe5vC#KrAE$xsXTsjYon7(OOm9|yuF~G^7i#sZ@^EkVx?TXSY3Xibe zwM}Y+pav@3l3qOd!IzF*4J~GCwdq`S`SRt7Q1PsJc4F(L+O&RkVJ*AGp~<{}p*x$V zD6;Ck+kF}Cp&a4^I=rS{G|25$1XFYL zisUzRk5Hz}zN7nx4eH1wqM1|_X+o2kSdl>F5-3;tJfnI>Lz{ zdodlX1S3+6cMc#KUL5ZkK=Q*E)3K)AaocUTI+$avNSy{9#IaUJFT+P|{I>Wzzs&BX z-Img$nQ$hW3a2wE1U7IH_K`$PiAPh2c2W>&SjgKq>+VSDEq&Tjdgl&z5VZjj9Ro`u zY9on&o-~3zn<{ugOy`r)l$a+~Jg+#F2)l{se@=4ux9*@=j8r_mg*X-#i}BsBetGPB zpE$uw=s!nM8>1|~BOVpEFrjwKuiW_NS8jZZN$Af`3T0^n$(Uu7w8VuAKP1^R=^i#q z)m7WAhZ`EVEgY5A3KQmGe4)~QxSI2}%$+&?fzWg$8;Q=W$4`!zL#dhQ_z6kLRIQYG z!b&cz&*eFpTje4+nsGp5_vwH6e!rx1=x{%hjxYF@52CSs`ThvaHfKJnKgPsJI)%E4 zI|;u1JKs}R4wU)e+j}Bo{c!IB5%AmXVQB(-!RnuuiZc6m9? zPiTUt!N3SDj|%7aLr%N1qnvhS+Ky9QhLMdqCYciy%K%|(5ZwwoD?;Fy-K3YRlA%>i z*dxzEdDW=&F2T?$NI8Vipq5XI8ge@FA2O&P2(&k~q^i+Qb`|Ny%NkFCIz!D#xLjL5 zEh>8JM;n>}WJL>8rmPzkM7rW*lpO{q#|u??oeC`o=OL?>pQJ8KIQn&v@TAhjYC?swS ziWTZ4nR>NK8DhrBFv+BEY;n9}`i3wjuw7E39xfl9s}DP734U4a%tV=FCQ2w!VKEv* zbQ@=)Or$8xU&`n6FkvL3&f!07dKEHJ4&2%Bz^tB~#q-dzB4Pn)_7X^;7PYUnom9c; zF|y8`&1Qe=P9#_PKJ1gi9nBzDQZniM;*+a1lZhN!0@?$shAfirq-=?n{Wr^|w&|T! zyn$~8QF{`8xU-*&-rI}9-;2Vh)L?rW+cu%BR6=0E{kNbGHbR(<~xe9sN3em4Xo zt4aW2`>Q#}vcC5I{kPrb=Hoek6HGPl$u${YaqJvLHWn&(gQN{esLHXowu**xYbyTV zqcevU|6An#M zAl>4nY8hFGoL4$sgP31I_WW7RD7I-(kr)!{QUah>$O1}OlyVUNp)#59v@b!Low-EW9FY+R-<2)) zJ3;X_^=c7?e+bM9n92sef|^Ux?{EH0Dpw}lXib=(p8*CNrpC` zMaH6^Z8$ID&DTHuW7%&pgL{h*mB;&Wtv+ZRM z>dmJ+AzkDa921>};Xc*OBZO35>eqMEoaP>nYMa)Ows6&Y|U;8}^~$?<44g%9r2u zuW$dctD8S+1898*0t3+cKEXEU&mxx8)z_ejp7m8FJIM5w<{kFTl@HnnM_>Er%<1`= zAa)PY%oY?%yKA4m3mOvTu#_<~TNX}l z%`CyH(%0)ew@6vhmJyk7Y2AQ(F%bA{TPNILT9GG&+)+M%5$S0YnXs6q zlt+l40nDXoUslr5OfHQaVd-2t8jHC}Tqg%g;tDG<8H894qSz3EVwo#7aHMa zqir&bmrNPmZK%r3un*2=e{UZw8}ET-<8SwqX1^&I|JA?v_3tr>YPY=`kwiEGZ$pyG zA>d^-$6JU;6d1YEF&MgX$+T0HV0XM5Eq&VFjm{muQD1jo5y`lk9BNsw+k%jgr7i{_ zQYwz*TgX$@93T)83AjBXaypa9r&DR$&ehGs@tXuf`Udk`WRlS>_Nv@0->Zj-8uaDDn;pdnKUms4ez%G=RE`mlM2*1ha2B!ucchW`@_^ zVni1?fe^9tuA5|shipnlaxV}IvKWbr5HOcq``lLjzuvs|)Bion z!;jgorneY+^V%2rOt*mkyn>%sJ;kggNp1ECf&@wKCg`3(k8}mZ?yXQAWiDY~IvmI9 zk0!a5pAjWVR8V!K5KpF}5~T*G1ln$zS}e?`FIlB?(^!~~V`09$C1tm;FrT7@IrqZ3 z$;&GaCWy0^{@QPpz8kq>=PdQ>+ayN4&HvGatDutvbvJzn#X{dKujy*JDMI-IkVaZX zC{k=m_p)oc3MK;1kaiV$RSB9yk&Je%!KSX~p)?~#15uA~kW3rk%XgI!fvO4n5^ zJf)18W)qDeLZyEXT(?vN9K&Q(X|yN}rLB^YfsMZcrJgCU6-BQ=*~}}&Y90!&Qr4@h zD$Ob~yrObAwJ9NT(bnyhEt#s+5fSqAokn_HsJF;5i#&gppqb?-D2$N}4W44BL$|Om zN_}ZEd*QZC(!)U1s5BS$;4laSKvgUzkh{#1_DW?1 zPBw4~-yqKNB7Ji~(h=8y^O`=VHBSVVR2UK1V#Qc-x-Fj!?_nIGv*A6AJv+W7Da@1Ol#i_!XEydR^rYwMhJ z7n!StIG$`9X>qZbKV2q$We+azBi%}JqJc6upFkWO%OBRHf68?#Yz`^iGsqRJJx$~pOpg@~Xf<&jb$ zOp-{EjF=8FVu0?{6h1k=M7C?NlS3`IC!4uCCX1#HS!zQ$LDJQ%POlEK(JTs3(g({@ zktD$Ik^#2?1j;f^&+#mzJ%hYW3>>rov4S2uPJ#raw=;-dluV3@6)+zYrVNc*+P5%C z6|Po?L8>^B6t-T_DG8o>wQRu;VN2P>dzp%cLxzA9h552SFHjB(lF#r&bH3e)V*Uuq zUE<{YmxR$g6nro!%Qp#6qPA8y{&E$2Y< z0Nj~1D;h?Q>2QjGItXY=h@7z`hDr}&L=sa<5;l|EZdkJh?`;PdX#|XP9a~&fauKDF zh)D@KEoF8dvKSkpx7qBk-GTdBIDq@Q;X#7B-`7pU*)NS={}Q{LciV6lO@vACNC6cV zOQt-#66~4@_=nO`DiV(-3prbE+a1GMOP{vktaFDu4&DReEfGm#j0n(;apOaY)*Nae z#F9#?5SKEYkilyvuv(5|r;1-^?eqM$+txPtzp7l`_kVi;RN*=*wI zAN}@+zW3I5@&>jq!W4J@KNHqC6HRPI<7u+5O&tBhU;f4Q7rxC9#BcMiF?Kr(&i(I1 z<6U?x`t-4EBr<($CARir_5;}uY*m*}Q(pITm)2giQdWvHym8TZO{#R*^p3{!ntCyA zM}3k4(Riv=V)GgB{LMW&hj0s#%s><#?@7UJ=*Gy|8S0x`D>}90^x;<)0MJ z@9P@Jb>(}u%rI3ofR43FcYa>AKV zLd6YoT9dRYhR3(AmRqWDADB^g>`_U;gEM~@xmgjngBBdX0xb}e5R9u zL}F&DkZ=j{b0ph9grM2Se+;A?lVCJ*#ek<^ZG+7O_5)OBFj(#}GBxzD2XuNzl$r$| znjl8+kme_#ueSmJyBvsjj}hkB=_aE;6&;`W*pmnV2-gpL8;PX!hrB;0{Ikh&LS->z zNRSKe_U$O7-_=$)>gVn3fR})Lf8#^H_0{|vzbZJyk=zWY0BlWUCq%xCim)n`$O1{J z!boB)p{^;cSujyS@S}=SSV9Gd=2w`T7LlAzab(oKUW3O%AlaQb^4u%w zZ@*fWQ3<5vE+afJJ2@Q&TzWBqY$K2Q5RB>T0r3RF1GD2S&>+qlhxEWKv26m?DF~ii z+5^em|MEU+Hby_H>(v$oMyj^M8Z8(Ae!u0X2W#e$%!z4UzK< z9%$2!eg6S~BBh=jx$D0RD6*L$2=GT1A>^PX7VGztH4p+X*-V+S3iEO09nfOiri6ly zR^3)C?8wOyLOEox72?TwG8wXEtCgl~7065xE1gE{K}dTu+UZ#?SwW|9xR10=;M=nB zF2g@f3c{oe$7V^BLQ1|$>*_DsCNU3urNp%g+w&Fdbh*6qi^H3$-Yj)`mYXWmavQWw z%-vX0WRYG(vdf}m(7HO@X`^%Ckk?o*&q9WGHGo9=wRd)NQSS#0Uk^S*Vh(>`O5-K>kD=JEgZiD;{#=I#9$YW#ud(9U(F$M#!k)=a6i2`Cqg zGX${1g7U012&0ImV*t9x2MvtjgF#YXe&|}rUj_Q;UHx$Hx<@a#l1g6EjPYhDh<_Y7 zSBf=6Sdi(UWhBXnD2gf6ElnmwW^X#xo8W|x7Bf7xkPyxvg5l||&S4>cVC%uQdxcOp zBtbJMiIA%azIZ})oX}dM?Rk{lJ!Lzy!V~An`8I2 z@kcUutZq9X_j#PN-K;5{cbjbzy)fTgjm|oRi_lc@XFSUhJv_@zca|jI?8`A(Q)OHvGt)Dy0?TZYFMzVOE?Y;O&G7? z$&OanBY?fqKW?(UXRdsmWX8&=c?9<1s3iF41)QT**jwQ}2TGz$*gE85t`WL#8G97p zT4c{h_WsJLGZLVx_--2Ur!v3-V}e5nHw(`8>>~XQ*L{f~1yg#dQne*de8+Vo9G|+7 zU38%Xrxm#TS@<1;8^B>N3Da3tMgrDd1>gq44*+{$BM0$4)v#!Z$3v<_BvU5dLI_B7 zm?pg;tT=bPLt^)j<}S&jy_2~&WA3`?b~3wsyX~A4m_)><2arp4Vw;Jkli_$08J`jn zWH=Gw!{+LB$CGetXQBmUwaUYS;rOqIx6KUE+3-k)o}Cr)X;~C==|UzKi6nAVW}R^F zGXupOxq(3t3z68i$2@lFZ8rOscf&j*l6eLhv!3ggI{?T-&pdO-vXUc(`9JvNn(g<< zP?xb!Lgi!%TATsz4Q@~P%`gYEN?xm$Xb%22-yBe?cM1WjmY6)}i0%N7m+fO>*Jq%6 zZuwqpxC85QD7asD(0Uzj{d1oHrUIV4{Uo!~S@E!YR&-Jo=mL5xwVd#OOSlGPyx|sB zxmY1;ZO|#7a!QRnmG(ZUk&dhd>7HaQq7ZWV>8#$iJ>th(>P}*>2`NH=;6m zg|Y@bQg4PccI>{{(tG+IS6gwE6(Ta$a}2EyxXOAnvTD|SytnASt!z#)Ru50w>WI)( zM+8qDY0F-GH@#~C`i-za3Qj|P#d{xJm~o7g+6GgJkQw>adK;B%^>@*bu*EtP;Zql% z|K3i+l#q%r4!}_}KrfNV$c1<;n!^vW$x(_eK1^wDCbs!f?DX1u^s5P@d`X%wUz`K7 zQ?Hq0=jV?_%5#f}_AsT*zw^*c{1TJc4ttgl+Ai<#{%m4b9SZ}=X-A$zY?81jm8wdy zlVtX%M<_EOFPYU(K)$*NU0j$(lN^zMe4^CQk)^w)0_KzM#leQay}7BCzRxlD=-Yv*$*N(_dyB@ zc#W>+FWuhf)jR?}Btn&tG6i?jA;|84>mc#nvYUcMUF1UhF4g~_?e^lsFugZ>3-7JY z47gWPpEp+1VRFbR{X*06jkdv3gdYr!*hC?%T+nx4c<+8aw{|_?0R4t&E@#{;jrzSS z&|owmprJLblqt`T`nR53z%~16^)u>c)z8%rmZYr}E#J5?;+k@iOI&%SaeVEUU@Tnw PNBBSW6Y8g`xzPUysggUA literal 0 HcmV?d00001 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'