From d832e3a93e3e7c682891eb310cba83754345b356 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 6 Jan 2023 23:37:41 -0800 Subject: [PATCH] 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) --- diyalgo/__init__.py | 1 + diyalgo/config.py | 13 +++- diyalgo/db.py | 31 ++++++++++ diyalgo/models/account.py | 2 +- diyalgo/models/status.py | 12 +++- poetry.lock | 124 ++++++++++++++++++++++++++++++++++++- pyproject.toml | 5 ++ tests/__init__.py | 0 tests/fixtures/__init__.py | 0 tests/fixtures/client.py | 24 +++++++ tests/fixtures/config.py | 9 +++ tests/fixtures/db.py | 26 ++++++++ tests/test_client.py | 10 +++ tests/test_model.py | 62 +++++++++++++++++++ 14 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 diyalgo/db.py create mode 100644 tests/__init__.py create mode 100644 tests/fixtures/__init__.py create mode 100644 tests/fixtures/client.py create mode 100644 tests/fixtures/config.py create mode 100644 tests/fixtures/db.py create mode 100644 tests/test_client.py create mode 100644 tests/test_model.py diff --git a/diyalgo/__init__.py b/diyalgo/__init__.py index 3f7333e..d695abb 100644 --- a/diyalgo/__init__.py +++ b/diyalgo/__init__.py @@ -1 +1,2 @@ from diyalgo.config import Config +from diyalgo.client.init import log_in \ No newline at end of file diff --git a/diyalgo/config.py b/diyalgo/config.py index 985bad2..97fd789 100644 --- a/diyalgo/config.py +++ b/diyalgo/config.py @@ -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' diff --git a/diyalgo/db.py b/diyalgo/db.py new file mode 100644 index 0000000..238addc --- /dev/null +++ b/diyalgo/db.py @@ -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) \ No newline at end of file diff --git a/diyalgo/models/account.py b/diyalgo/models/account.py index f7b9958..8302c28 100644 --- a/diyalgo/models/account.py +++ b/diyalgo/models/account.py @@ -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') diff --git a/diyalgo/models/status.py b/diyalgo/models/status.py index 91f315f..466203f 100644 --- a/diyalgo/models/status.py +++ b/diyalgo/models/status.py @@ -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' diff --git a/poetry.lock b/poetry.lock index 8b00aa3..c638f2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 418e449..382ee78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/client.py b/tests/fixtures/client.py new file mode 100644 index 0000000..e6239f8 --- /dev/null +++ b/tests/fixtures/client.py @@ -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() + diff --git a/tests/fixtures/config.py b/tests/fixtures/config.py new file mode 100644 index 0000000..0378c52 --- /dev/null +++ b/tests/fixtures/config.py @@ -0,0 +1,9 @@ +import pytest + +from diyalgo import Config + +@pytest.fixture() +def config_fixture() -> Config: + return Config( + DB=None # use in_RAM database + ) \ No newline at end of file diff --git a/tests/fixtures/db.py b/tests/fixtures/db.py new file mode 100644 index 0000000..f6ceac7 --- /dev/null +++ b/tests/fixtures/db.py @@ -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) + + + diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..1f80ad0 --- /dev/null +++ b/tests/test_client.py @@ -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) + + diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..03bddd5 --- /dev/null +++ b/tests/test_model.py @@ -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() \ No newline at end of file