Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 100 additions & 12 deletions manual_testing/mastodon_manual_test.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,95 @@
import sys, os
import time
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from abstractions import Post
from abstractions import AdoptablePet
from social_posters.mastodon import PosterMastodon

def post_exceed_500_chars_limit_with_adoption_link():
pet = AdoptablePet("Brian",
"Labrador Retriever",
"White Labrador",
"Quahog",
"I am a writer! Post exceeds limit with adoption link"*200,
"http://www.davidgorman.com/4quartets/",
"https://static.wikia.nocookie.net/familyguy/images/c/c2/FamilyGuy_Single_BrianWriter_R7.jpg/revision/latest?cb=20230807152447",
11,
"Male",
None,
None
)
return pet

def post_exceed_500_chars_limit_without_adoption_link():
pet = AdoptablePet("Vinny",
"Unknown",
"Unknown",
"Quahog",
"I am 1/16th cat! Post exceeds word limit without adoption link."*1000,
None,
"https://static.wikia.nocookie.net/familyguyfanon/images/e/ec/Vinny_Griffin.png/revision/latest?cb=20161129110103",
None,
"Male",
None,
None
)
return pet


def post_within_500_chars_limit_with_adoption_link():
pet = AdoptablePet("Ernie",
"Chicken",
"Unknown",
"Quahog",
"cluck. Post within word limit with adoption link.",
"https://poets.org/poem/having-coke-you",
"https://static.wikia.nocookie.net/villains/images/2/2e/Giant_chicken_animation.png/revision/latest?cb=20220615120124",
None,
"Male",
None,
None
)
return pet

def post_within_500_chars_limit_without_adoption_link():
pet = AdoptablePet("Pouncy",
"Cat",
"Unknown",
"Quahog",
"Meow. Post within 500 limit without adoption link",
None,
"https://static.wikia.nocookie.net/villains/images/7/76/Pouncey.webp/revision/latest?cb=20220403224856",
None,
"Male",
None,
None
)
return pet

def post_unicode():
pet = AdoptablePet("Vinny",
"Unknown",
"Unknown",
"Quahog",
"🐶❤️ 可爱的小狗 Friendly \"lap cat\" @ shelter #AdoptMe",
None,
"https://static.wikia.nocookie.net/familyguyfanon/images/e/ec/Vinny_Griffin.png/revision/latest?cb=20161129110103",
None,
"Male",
None,
None
)
return pet

# !!!DO NOT TEST MULTIPLE CASES AT THE SAME TIME!!!
# !!!OTHERWISE YOU MAY TRIGGER SPAM DETECTION!!!
testingCases = [
post_exceed_500_chars_limit_with_adoption_link,
post_exceed_500_chars_limit_without_adoption_link,
post_within_500_chars_limit_with_adoption_link,
post_within_500_chars_limit_without_adoption_link,
post_unicode
]

def main():
poster = PosterMastodon()
Expand All @@ -15,19 +100,22 @@ def main():

print("Authenticated to Mastodon!")

post = Post(
text="Test post",
image_url="https://static.wikia.nocookie.net/familyguy/images/c/c2/FamilyGuy_Single_BrianWriter_R7.jpg/revision/latest?cb=20230807152447",
alt_text="Cute animal",
tags=["Test", "Mastodon"],
)
for pet in testingCases:
pet_instance = pet()

post = poster.format_post(pet_instance)
target_url = pet_instance.adoption_url
if target_url and (target_url not in post.text):
print("Adoption link not posted!")

result = poster.publish(post)

result = poster.publish(post)
if result.success:
print(f"Posted successfully! URL: {result.post_url}")
else:
print(f"Post failed: {result.error_message}")
time.sleep(1)

if result.success:
print(f"Posted successfully! URL: {result.post_url}")
else:
print(f"Post failed: {result.error_message}")

if __name__ == "__main__":
main()
207 changes: 207 additions & 0 deletions manual_testing/mastodon_preview_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""
Mastodon formatting preview tool:

This file visualizes different stages of the Mastodon formatting pipeline.

Pipeline structure:
AdoptablePet
-> Post
-> PreparedCaption
-> CaptionThread

Examples:

Preview raw pet input:
python manual_tests/mastodon_preview.py --stage pet

Preview generated platform-independent Post:
python manual_tests/mastodon_preview.py --stage post

Preview fully formatted Mastodon thread:
python manual_tests/mastodon_preview.py --stage debug

Preview only the main Mastodon post:
python manual_tests/mastodon_preview.py --stage main

Preview only reply thread chunks:
python manual_tests/mastodon_preview.py --stage replies

Preview every pipeline stage:
python manual_tests/mastodon_preview.py --stage all

(default behavior is --stage all)

To extend preview stages:
1. Add a new PreviewStage enum entry
2. Add a renderer/action for that stage
"""

from __future__ import annotations

import argparse
import os
import sys
from dataclasses import asdict, is_dataclass
from enum import StrEnum
from pprint import pprint

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from mastodon_manual_test import post_exceed_500_chars_limit_with_adoption_link
from social_posters.mastodon import CaptionThread, MastodonPhase, PosterMastodon
from utils.pipeline import Phase, PipelineResult
from utils.pipeline_preview import PreviewSection, print_section, render_sections


class PreviewStage(StrEnum):
PET = "pet"
POST = "post"
PREPARED_CAPTION = "prepared_caption"
CAPTION_THREAD = "caption_thread"
MAIN = "main"
REPLIES = "replies"
TRACE = "trace"
ALL = "all"


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Preview each stage of the Mastodon formatting pipeline."
)
parser.add_argument(
"--stage",
type=PreviewStage,
choices=list(PreviewStage),
default=PreviewStage.ALL,
help="Which construction stage to preview.",
)
return parser.parse_args()


def find_phase(pipeline: PipelineResult, phase_name: MastodonPhase) -> Phase | None:
for phase in pipeline.trace:
if phase.name == phase_name:
return phase

return None


def value_for_phase(pipeline: PipelineResult, phase_name: MastodonPhase) -> object | None:
phase = find_phase(pipeline, phase_name)
return phase.value if phase else None


def print_value(value: object) -> None:
if is_dataclass(value) and not isinstance(value, type):
pprint(asdict(value))
else:
pprint(value)


def print_phase(pipeline: PipelineResult, phase_name: MastodonPhase) -> None:
value = value_for_phase(pipeline, phase_name)

if value is None:
print(f"(No value recorded for {phase_name})")
return

print_value(value)


def print_trace(pipeline: PipelineResult) -> None:
for i, phase in enumerate(pipeline.trace, start=1):
print_section(f"PHASE {i}: {phase.name}")
print_value(phase.value)

if pipeline.errors:
print_section("ERRORS")
for error in pipeline.errors:
print(f"{type(error).__name__}: {error}")


def print_main_caption(thread: CaptionThread | None) -> None:
if thread is None:
print("(No caption thread)")
return

print(thread.main_caption)
print(f"\nLength: {len(thread.main_caption)}")


def print_replies(thread: CaptionThread | None) -> None:
if thread is None:
print("(No caption thread)")
return

if not thread.replies:
print("(No replies)")
return

for i, reply in enumerate(thread.replies, start=1):
print_section(f"REPLY {i}")
print(reply)
print(f"\nLength: {len(reply)}")


def main() -> None:
args = parse_args()
selected_stage: PreviewStage = args.stage

poster = PosterMastodon.__new__(PosterMastodon)

pet = post_exceed_500_chars_limit_with_adoption_link()
pipeline = poster.build_formatting_pipeline(pet)

thread = (
pipeline.value
if pipeline.ok and isinstance(pipeline.value, CaptionThread)
else None
)

sections = [
PreviewSection(
PreviewStage.PET,
"PET",
lambda: print_phase(pipeline, MastodonPhase.PET),
),
PreviewSection(
PreviewStage.POST,
"POST OBJECT",
lambda: print_phase(pipeline, MastodonPhase.POST),
),
PreviewSection(
PreviewStage.PREPARED_CAPTION,
"PREPARED CAPTION",
lambda: print_phase(pipeline, MastodonPhase.PREPARED_CAPTION),
),
PreviewSection(
PreviewStage.CAPTION_THREAD,
"CAPTION THREAD",
lambda: print_phase(pipeline, MastodonPhase.CAPTION_THREAD),
),
PreviewSection(
PreviewStage.MAIN,
"MAIN POST",
lambda: print_main_caption(thread),
),
PreviewSection(
PreviewStage.REPLIES,
"REPLIES",
lambda: print_replies(thread),
),
PreviewSection(
PreviewStage.TRACE,
"FULL TRACE",
lambda: print_trace(pipeline),
),
]

render_sections(
sections=sections,
selected=selected_stage,
all_stage=PreviewStage.ALL,
)


if __name__ == "__main__":
main()
28 changes: 11 additions & 17 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ api-display-purposes==0.0.3
attrs==25.4.0
beautifulsoup4==4.14.3
blurhash==1.1.5
build==1.5.0
certifi==2026.2.25
chardet==3.0.4
charset-normalizer==3.4.4
clarifai==2.6.2
click==8.3.3
configparser==3.8.1
decorator==4.0.2
EasyProcess==1.1
Expand All @@ -17,36 +19,28 @@ grpcio==1.78.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
hypothesis==6.152.7
idna==2.10
iniconfig==2.3.0
instapy==0.6.16
jsonschema==2.6.0
Mastodon.py==2.1.4
MeaningCloud-python==2.0.0
outcome==1.3.0.post0
packaging==26.2
pip-tools==7.5.3
pluggy==1.6.0
plyer==2.1.0
protobuf==3.20.3
Pygments==2.20.0
pyproject_hooks==1.2.0
PySocks==1.7.1
pytest==9.0.3
python-dateutil==2.9.0.post0
python-magic==0.4.27
python-telegram-bot==22.6
PyVirtualDisplay==3.0
PyYAML==6.0.3
regex==2026.2.28
requests==2.32.5
selenium==4.41.0
semantic-version==2.10.0
requests==2.33.1
setuptools==82.0.0
setuptools-rust==1.12.0
six==1.17.0
sniffio==1.3.1
sortedcontainers==2.4.0
soupsieve==2.8.3
tqdm==4.67.3
trio==0.33.0
trio-websocket==0.12.2
typing_extensions==4.15.0
urllib3==2.6.3
webdriverdownloader==1.1.0.4
websocket-client==1.9.0
wsproto==1.3.2
wheel==0.47.0
Loading
Loading