Got a functional status and account model and wrote a test for validating the status (but still requires an active vagrant instance which it does not create)

This commit is contained in:
sneakers-the-rat 2023-01-06 23:37:41 -08:00
parent b156c57ff1
commit d832e3a93e
14 changed files with 315 additions and 4 deletions

View File

@ -1 +1,2 @@
from diyalgo.config import Config
from diyalgo.client.init import log_in

View File

@ -5,7 +5,18 @@ from pydantic import BaseSettings, AnyHttpUrl, EmailStr
class Config(BaseSettings):
MASTO_URL:AnyHttpUrl
MASTO_TOKEN: Optional[str] = None
LOGDIR:Path = Path().home() / '.mastotools'
LOGDIR:Path = Path().home() / '.diyalgo'
DB:Optional[Path] = Path().home() / '.diyalgo' / 'diyalgo.db'
"""
Optional, if set to ``None`` , use the in-memory sqlite DB
"""
@property
def sqlite_path(self) -> str:
if self.DB is None:
return 'sqlite://'
else:
return f'sqlite:///{str(self.DB.resolve())}'
class Config:
env_file = '.env'

31
diyalgo/db.py Normal file
View File

@ -0,0 +1,31 @@
"""
Initialization routines for the database
"""
from typing import Optional, TYPE_CHECKING
# This should also import all the public models
# so the engine is ready for them
# https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/
from diyalgo import models, Config
from sqlmodel import SQLModel, create_engine
if TYPE_CHECKING:
from sqlalchemy.future.engine import Engine
def init_db_engine(config:Optional[Config] = None) -> 'Engine':
if config is None:
config = Config()
engine = create_engine(config.sqlite_path)
return engine
def create_tables(
engine: Optional['Engine'] = None,
config:Optional[Config] = None
):
if engine is None:
engine = init_db_engine(config=config)
SQLModel.metadata.create_all(engine)

View File

@ -19,7 +19,7 @@ class Account(SQLModel, table=True):
avatar_static: str
bot: bool
# created_at:datetime
discoverable:bool
discoverable:Optional[bool] = None
display_name:str
emojis: List['CustomEmoji'] = Relationship(back_populates='accounts', link_model=EmojiAccountLink)
# fields: List["AccountField"] = Relationship(back_populates='account')

View File

@ -35,7 +35,7 @@ class Status(SQLModel, table=True):
emojis: List['CustomEmoji'] = Relationship(back_populates='statuses', link_model=EmojiStatusLink)
favourited: Optional[bool] = None
favourites_count: int
filtered: List[str] = Field(default_factory=list)
#filtered: List[str] = Field(default_factory=list)
in_reply_to_id: Optional[int] = None
in_reply_to_account_id: Optional[int] = None
language: Optional[str] = None
@ -60,6 +60,16 @@ class Status(SQLModel, table=True):
def soup(self) -> BeautifulSoup:
return BeautifulSoup(self.content)
def __init__(self, *args, **kwargs):
from diyalgo.models import Account, MediaAttachment, Tag, CustomEmoji, Poll
#kwargs['account'] = Account(**kwargs['account'])
kwargs['account_id'] = kwargs['account']['id']
del kwargs['account']
kwargs['emojis'] = [CustomEmoji(**e) for e in kwargs['emojis']]
kwargs['media_attachments'] = [MediaAttachment(**a) for a in kwargs['media_attachments']]
kwargs['mentions'] = [Mention(**m) for m in kwargs['mentions']]
kwargs['tags'] = [Tag(**t) for t in kwargs['tags']]
super(Status, self).__init__(*args, **kwargs)
class Config:
extra='ignore'

124
poetry.lock generated
View File

@ -1,5 +1,24 @@
# This file is automatically @generated by Poetry and should not be changed by hand.
[[package]]
name = "attrs"
version = "22.2.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
{file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
]
[package.extras]
cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
dev = ["attrs[docs,tests]"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
tests = ["attrs[tests-no-zope]", "zope.interface"]
tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
[[package]]
name = "beautifulsoup4"
version = "4.11.1"
@ -61,6 +80,18 @@ files = [
[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"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "decorator"
version = "5.1.1"
@ -73,6 +104,21 @@ files = [
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]]
name = "exceptiongroup"
version = "1.1.0"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "greenlet"
version = "2.0.1"
@ -159,6 +205,18 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
[[package]]
name = "lxml"
version = "4.9.2"
@ -277,6 +335,34 @@ blurhash = ["blurhash (>=1.1.4)"]
test = ["blurhash (>=1.1.4)", "cryptography (>=1.6.0)", "http-ece (>=1.0.5)", "pytest", "pytest-cov", "pytest-mock", "pytest-runner", "pytest-vcr", "pytz", "requests-mock", "vcrpy"]
webpush = ["cryptography (>=1.6.0)", "http-ece (>=1.0.5)"]
[[package]]
name = "packaging"
version = "22.0"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
{file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
]
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pydantic"
version = "1.10.4"
@ -330,6 +416,30 @@ typing-extensions = ">=4.2.0"
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pytest"
version = "7.2.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
]
[package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
@ -525,6 +635,18 @@ pydantic = ">=1.8.2,<2.0.0"
SQLAlchemy = ">=1.4.17,<=1.4.41"
sqlalchemy2-stubs = "*"
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.4.0"
@ -557,4 +679,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "bcb1da145e3fb10a5e98a37ae6f4e9933e32b3152fe17799e964187294f7a890"
content-hash = "3504f840231279fd4e2ef50e2990824d644e1b9e0720eaf0059887d4de26baf6"

View File

@ -18,6 +18,11 @@ beautifulsoup4 = "^4.11.1"
lxml = "^4.9.2"
python-dotenv = "^0.21.0"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
[build-system]
requires = ["poetry-core"]

0
tests/__init__.py Normal file
View File

0
tests/fixtures/__init__.py vendored Normal file
View File

24
tests/fixtures/client.py vendored Normal file
View File

@ -0,0 +1,24 @@
import pytest
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from mastodon import Mastodon
from diyalgo import Config
from diyalgo.client.init import log_in
@pytest.fixture(scope="session")
def client_fixture(config:Optional['Config'] = None) -> 'Mastodon':
"""
A logged in client with the default credentials (in an .env file atm)
Returns:
"""
client = log_in(config)
yield client
client.session.close()

9
tests/fixtures/config.py vendored Normal file
View File

@ -0,0 +1,9 @@
import pytest
from diyalgo import Config
@pytest.fixture()
def config_fixture() -> Config:
return Config(
DB=None # use in_RAM database
)

26
tests/fixtures/db.py vendored Normal file
View File

@ -0,0 +1,26 @@
import pytest
from sqlalchemy.future.engine import Engine
from sqlmodel import SQLModel, Session
from .config import config_fixture
from diyalgo import models
from diyalgo.db import init_db_engine, create_tables
@pytest.fixture()
def engine_fixture(config_fixture) -> Engine:
"""Create and engine and then create tables"""
engine = init_db_engine(config_fixture)
SQLModel.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture()
def session_fixture(engine_fixture) -> Session:
return Session(engine_fixture)

10
tests/test_client.py Normal file
View File

@ -0,0 +1,10 @@
import pytest
from .fixtures.client import client_fixture as client
def test_log_in(client):
assert client.access_token is not None
assert isinstance(client._Mastodon__get_logged_in_id(), int)

62
tests/test_model.py Normal file
View File

@ -0,0 +1,62 @@
import pytest
import pdb
from datetime import datetime, timezone
from .fixtures.config import config_fixture
from .fixtures.client import client_fixture as client_fixture
from .fixtures.db import session_fixture, engine_fixture
from sqlmodel import select
from diyalgo.models import Status
@pytest.mark.parametrize('status_str', ['My test status'])
def test_status_text(client_fixture, session_fixture, status_str):
posted = client_fixture.status_post(status_str)
status = Status(**posted)
with session_fixture as session:
session.add(status)
session.commit()
# compare to committed status
status = session.exec(select(Status).where(Status.id == posted['id'])).first()
# model uses account_id instead of full account object
posted['account_id'] = posted['account']['id']
for k, v in status.dict().items():
# text is a special case that's only relevant when the status has been deleted:
# https://docs.joinmastodon.org/entities/Status/#text
if k in ('text', 'account'):
continue
# we gain utc timezone for datetimes
if isinstance(v, datetime):
assert v == posted[k].replace(tzinfo=None)
else:
assert v == posted[k]
# if __name__ == "__main__":
# from diyalgo.client.init import log_in
# from diyalgo import models
# from diyalgo.db import init_db_engine, create_tables
# from sqlmodel import Session
#
# client = log_in()
# posted = client.status_post('hey my status')
# status = models.Status(**posted)
#
# engine = init_db_engine()
# create_tables(engine)
# session = Session(engine)
#
# session.add(status)
# session.commit()
#
# posted2 = client.status_post('hey my status again')
# status2 = models.Status(**posted2)
# session.add(status2)
# session.commit()