Update.
This commit is contained in:
parent
5a0a066105
commit
52df3887a6
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
|||||||
.history
|
.history
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
examples/crawler/devrant.sqlite-shm
|
examples/crawler/devrant.sqlite-shm
|
||||||
examples/crawler/devrant.sqlite-wal
|
examples/crawler/devrant.sqlite-wal
|
||||||
examples/crawler/devrant.sqlite
|
examples/crawler/devrant.sqlite
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from devranta.api import Api, Rant
|
|
||||||
from database import DatabaseManager
|
from database import DatabaseManager
|
||||||
|
|
||||||
|
from devranta.api import Api, Rant
|
||||||
|
|
||||||
|
|
||||||
class DevRantCrawler:
|
class DevRantCrawler:
|
||||||
def __init__(self, api: Api, db: DatabaseManager, rant_consumers: int, user_consumers: int):
|
def __init__(
|
||||||
|
self, api: Api, db: DatabaseManager, rant_consumers: int, user_consumers: int
|
||||||
|
):
|
||||||
self.api = api
|
self.api = api
|
||||||
self.db = db
|
self.db = db
|
||||||
self.rant_queue = asyncio.Queue(maxsize=1000000)
|
self.rant_queue = asyncio.Queue(maxsize=1000000)
|
||||||
@ -18,23 +23,29 @@ class DevRantCrawler:
|
|||||||
self.seen_rant_ids: Set[int] = set()
|
self.seen_rant_ids: Set[int] = set()
|
||||||
self.seen_user_ids: Set[int] = set()
|
self.seen_user_ids: Set[int] = set()
|
||||||
self.stats = {
|
self.stats = {
|
||||||
"rants_processed": 0, "rants_added_to_db": 0,
|
"rants_processed": 0,
|
||||||
"comments_added_to_db": 0, "users_processed": 0, "users_added_to_db": 0,
|
"rants_added_to_db": 0,
|
||||||
"api_errors": 0, "producer_loops": 0, "end_of_feed_hits": 0,
|
"comments_added_to_db": 0,
|
||||||
"rants_queued": 0, "users_queued": 0
|
"users_processed": 0,
|
||||||
|
"users_added_to_db": 0,
|
||||||
|
"api_errors": 0,
|
||||||
|
"producer_loops": 0,
|
||||||
|
"end_of_feed_hits": 0,
|
||||||
|
"rants_queued": 0,
|
||||||
|
"users_queued": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _queue_user_if_new(self, user_id: int):
|
async def _queue_user_if_new(self, user_id: int):
|
||||||
if user_id in self.seen_user_ids:
|
if user_id in self.seen_user_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.seen_user_ids.add(user_id)
|
self.seen_user_ids.add(user_id)
|
||||||
if not await self.db.user_exists(user_id):
|
if not await self.db.user_exists(user_id):
|
||||||
await self.user_queue.put(user_id)
|
await self.user_queue.put(user_id)
|
||||||
self.stats["users_queued"] += 1
|
self.stats["users_queued"] += 1
|
||||||
|
|
||||||
async def _queue_rant_if_new(self, rant_obj: Rant):
|
async def _queue_rant_if_new(self, rant_obj: Rant):
|
||||||
rant_id = rant_obj['id']
|
rant_id = rant_obj["id"]
|
||||||
if rant_id in self.seen_rant_ids:
|
if rant_id in self.seen_rant_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -49,52 +60,64 @@ class DevRantCrawler:
|
|||||||
logging.info("Starting initial seeder to re-ignite crawling process...")
|
logging.info("Starting initial seeder to re-ignite crawling process...")
|
||||||
user_ids = await self.db.get_random_user_ids(limit=2000)
|
user_ids = await self.db.get_random_user_ids(limit=2000)
|
||||||
if not user_ids:
|
if not user_ids:
|
||||||
logging.info("Seeder found no existing users. Crawler will start from scratch.")
|
logging.info(
|
||||||
|
"Seeder found no existing users. Crawler will start from scratch."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
for user_id in user_ids:
|
for user_id in user_ids:
|
||||||
if user_id not in self.seen_user_ids:
|
if user_id not in self.seen_user_ids:
|
||||||
self.seen_user_ids.add(user_id)
|
self.seen_user_ids.add(user_id)
|
||||||
await self.user_queue.put(user_id)
|
await self.user_queue.put(user_id)
|
||||||
self.stats["users_queued"] += 1
|
self.stats["users_queued"] += 1
|
||||||
logging.info(f"Seeder finished: Queued {len(user_ids)} users to kickstart exploration.")
|
logging.info(
|
||||||
|
f"Seeder finished: Queued {len(user_ids)} users to kickstart exploration."
|
||||||
|
)
|
||||||
|
|
||||||
async def _rant_producer(self):
|
async def _rant_producer(self):
|
||||||
logging.info("Rant producer started.")
|
logging.info("Rant producer started.")
|
||||||
skip = 0
|
skip = 0
|
||||||
consecutive_empty_responses = 0
|
consecutive_empty_responses = 0
|
||||||
|
|
||||||
while not self.shutdown_event.is_set():
|
while not self.shutdown_event.is_set():
|
||||||
try:
|
try:
|
||||||
logging.info(f"Producer: Fetching rants with skip={skip}...")
|
logging.info(f"Producer: Fetching rants with skip={skip}...")
|
||||||
rants = await self.api.get_rants(sort="recent", limit=50, skip=skip)
|
rants = await self.api.get_rants(sort="recent", limit=50, skip=skip)
|
||||||
self.stats["producer_loops"] += 1
|
self.stats["producer_loops"] += 1
|
||||||
|
|
||||||
if not rants:
|
if not rants:
|
||||||
consecutive_empty_responses += 1
|
consecutive_empty_responses += 1
|
||||||
logging.info(f"Producer: Feed returned empty. Consecutive empty hits: {consecutive_empty_responses}.")
|
logging.info(
|
||||||
|
f"Producer: Feed returned empty. Consecutive empty hits: {consecutive_empty_responses}."
|
||||||
|
)
|
||||||
if consecutive_empty_responses >= 5:
|
if consecutive_empty_responses >= 5:
|
||||||
self.stats["end_of_feed_hits"] += 1
|
self.stats["end_of_feed_hits"] += 1
|
||||||
logging.info("Producer: End of feed likely reached. Pausing for 15 minutes before reset.")
|
logging.info(
|
||||||
|
"Producer: End of feed likely reached. Pausing for 15 minutes before reset."
|
||||||
|
)
|
||||||
await asyncio.sleep(900)
|
await asyncio.sleep(900)
|
||||||
skip = 0
|
skip = 0
|
||||||
consecutive_empty_responses = 0
|
consecutive_empty_responses = 0
|
||||||
else:
|
else:
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
consecutive_empty_responses = 0
|
consecutive_empty_responses = 0
|
||||||
new_rants_found = 0
|
new_rants_found = 0
|
||||||
for rant in rants:
|
for rant in rants:
|
||||||
await self._queue_rant_if_new(rant)
|
await self._queue_rant_if_new(rant)
|
||||||
new_rants_found += 1
|
new_rants_found += 1
|
||||||
|
|
||||||
logging.info(f"Producer: Processed {new_rants_found} rants from feed. Total queued: {self.stats['rants_queued']}.")
|
logging.info(
|
||||||
|
f"Producer: Processed {new_rants_found} rants from feed. Total queued: {self.stats['rants_queued']}."
|
||||||
|
)
|
||||||
skip += len(rants)
|
skip += len(rants)
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.critical(f"Producer: Unhandled exception: {e}. Retrying in 60s.")
|
logging.critical(
|
||||||
|
f"Producer: Unhandled exception: {e}. Retrying in 60s."
|
||||||
|
)
|
||||||
self.stats["api_errors"] += 1
|
self.stats["api_errors"] += 1
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
@ -103,23 +126,29 @@ class DevRantCrawler:
|
|||||||
while not self.shutdown_event.is_set():
|
while not self.shutdown_event.is_set():
|
||||||
try:
|
try:
|
||||||
rant_id = await self.rant_queue.get()
|
rant_id = await self.rant_queue.get()
|
||||||
logging.info(f"Rant consumer #{worker_id}: Processing rant ID {rant_id}.")
|
logging.info(
|
||||||
|
f"Rant consumer #{worker_id}: Processing rant ID {rant_id}."
|
||||||
|
)
|
||||||
|
|
||||||
rant_details = await self.api.get_rant(rant_id)
|
rant_details = await self.api.get_rant(rant_id)
|
||||||
if not rant_details or not rant_details.get("success"):
|
if not rant_details or not rant_details.get("success"):
|
||||||
logging.warning(f"Rant consumer #{worker_id}: Failed to fetch details for rant {rant_id}.")
|
logging.warning(
|
||||||
|
f"Rant consumer #{worker_id}: Failed to fetch details for rant {rant_id}."
|
||||||
|
)
|
||||||
self.rant_queue.task_done()
|
self.rant_queue.task_done()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
await self._queue_user_if_new(rant_details['rant']['user_id'])
|
await self._queue_user_if_new(rant_details["rant"]["user_id"])
|
||||||
|
|
||||||
comments = rant_details.get("comments", [])
|
comments = rant_details.get("comments", [])
|
||||||
for comment in comments:
|
for comment in comments:
|
||||||
await self.db.add_comment(comment)
|
await self.db.add_comment(comment)
|
||||||
self.stats["comments_added_to_db"] += 1
|
self.stats["comments_added_to_db"] += 1
|
||||||
await self._queue_user_if_new(comment['user_id'])
|
await self._queue_user_if_new(comment["user_id"])
|
||||||
|
|
||||||
logging.info(f"Rant consumer #{worker_id}: Finished processing rant {rant_id}, found {len(comments)} comments.")
|
logging.info(
|
||||||
|
f"Rant consumer #{worker_id}: Finished processing rant {rant_id}, found {len(comments)} comments."
|
||||||
|
)
|
||||||
self.stats["rants_processed"] += 1
|
self.stats["rants_processed"] += 1
|
||||||
self.rant_queue.task_done()
|
self.rant_queue.task_done()
|
||||||
|
|
||||||
@ -132,17 +161,21 @@ class DevRantCrawler:
|
|||||||
while not self.shutdown_event.is_set():
|
while not self.shutdown_event.is_set():
|
||||||
try:
|
try:
|
||||||
user_id = await self.user_queue.get()
|
user_id = await self.user_queue.get()
|
||||||
logging.info(f"User consumer #{worker_id}: Processing user ID {user_id}.")
|
logging.info(
|
||||||
|
f"User consumer #{worker_id}: Processing user ID {user_id}."
|
||||||
|
)
|
||||||
|
|
||||||
profile = await self.api.get_profile(user_id)
|
profile = await self.api.get_profile(user_id)
|
||||||
if not profile:
|
if not profile:
|
||||||
logging.warning(f"User consumer #{worker_id}: Could not fetch profile for user {user_id}.")
|
logging.warning(
|
||||||
|
f"User consumer #{worker_id}: Could not fetch profile for user {user_id}."
|
||||||
|
)
|
||||||
self.user_queue.task_done()
|
self.user_queue.task_done()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
await self.db.add_user(profile, user_id)
|
await self.db.add_user(profile, user_id)
|
||||||
self.stats["users_added_to_db"] += 1
|
self.stats["users_added_to_db"] += 1
|
||||||
|
|
||||||
rants_found_on_profile = 0
|
rants_found_on_profile = 0
|
||||||
content_sections = profile.get("content", {}).get("content", {})
|
content_sections = profile.get("content", {}).get("content", {})
|
||||||
for section_name in ["rants", "upvoted", "favorites"]:
|
for section_name in ["rants", "upvoted", "favorites"]:
|
||||||
@ -150,13 +183,15 @@ class DevRantCrawler:
|
|||||||
await self._queue_rant_if_new(rant_obj)
|
await self._queue_rant_if_new(rant_obj)
|
||||||
rants_found_on_profile += 1
|
rants_found_on_profile += 1
|
||||||
|
|
||||||
logging.info(f"User consumer #{worker_id}: Finished user {user_id}, found and queued {rants_found_on_profile} associated rants.")
|
logging.info(
|
||||||
|
f"User consumer #{worker_id}: Finished user {user_id}, found and queued {rants_found_on_profile} associated rants."
|
||||||
|
)
|
||||||
self.stats["users_processed"] += 1
|
self.stats["users_processed"] += 1
|
||||||
self.user_queue.task_done()
|
self.user_queue.task_done()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"User consumer #{worker_id}: Unhandled exception: {e}")
|
logging.error(f"User consumer #{worker_id}: Unhandled exception: {e}")
|
||||||
self.user_queue.task_done()
|
self.user_queue.task_done()
|
||||||
|
|
||||||
async def _stats_reporter(self):
|
async def _stats_reporter(self):
|
||||||
logging.info("Stats reporter started.")
|
logging.info("Stats reporter started.")
|
||||||
while not self.shutdown_event.is_set():
|
while not self.shutdown_event.is_set():
|
||||||
@ -172,7 +207,7 @@ class DevRantCrawler:
|
|||||||
async def run(self):
|
async def run(self):
|
||||||
logging.info("Exhaustive crawler starting...")
|
logging.info("Exhaustive crawler starting...")
|
||||||
await self._initial_seed()
|
await self._initial_seed()
|
||||||
|
|
||||||
logging.info("Starting main producer and consumer tasks...")
|
logging.info("Starting main producer and consumer tasks...")
|
||||||
tasks = []
|
tasks = []
|
||||||
try:
|
try:
|
||||||
@ -181,7 +216,7 @@ class DevRantCrawler:
|
|||||||
|
|
||||||
for i in range(self.num_rant_consumers):
|
for i in range(self.num_rant_consumers):
|
||||||
tasks.append(asyncio.create_task(self._rant_consumer(i + 1)))
|
tasks.append(asyncio.create_task(self._rant_consumer(i + 1)))
|
||||||
|
|
||||||
for i in range(self.num_user_consumers):
|
for i in range(self.num_user_consumers):
|
||||||
tasks.append(asyncio.create_task(self._user_consumer(i + 1)))
|
tasks.append(asyncio.create_task(self._user_consumer(i + 1)))
|
||||||
|
|
||||||
@ -190,7 +225,7 @@ class DevRantCrawler:
|
|||||||
logging.info("Crawler run cancelled.")
|
logging.info("Crawler run cancelled.")
|
||||||
finally:
|
finally:
|
||||||
await self.shutdown()
|
await self.shutdown()
|
||||||
|
|
||||||
async def shutdown(self):
|
async def shutdown(self):
|
||||||
if self.shutdown_event.is_set():
|
if self.shutdown_event.is_set():
|
||||||
return
|
return
|
||||||
@ -207,8 +242,7 @@ class DevRantCrawler:
|
|||||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
logging.info("All tasks cancelled.")
|
logging.info("All tasks cancelled.")
|
||||||
logging.info(f"--- FINAL STATS ---\n{self.stats}")
|
logging.info(f"--- FINAL STATS ---\n{self.stats}")
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import aiosqlite
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from devranta.api import Rant, Comment, UserProfile
|
|
||||||
|
import aiosqlite
|
||||||
|
|
||||||
|
from devranta.api import Comment, Rant, UserProfile
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
class DatabaseManager:
|
||||||
def __init__(self, db_path: str):
|
def __init__(self, db_path: str):
|
||||||
@ -24,7 +27,8 @@ class DatabaseManager:
|
|||||||
|
|
||||||
async def create_tables(self):
|
async def create_tables(self):
|
||||||
logging.info("Ensuring database tables exist...")
|
logging.info("Ensuring database tables exist...")
|
||||||
await self._conn.executescript("""
|
await self._conn.executescript(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
@ -52,45 +56,75 @@ class DatabaseManager:
|
|||||||
score INTEGER,
|
score INTEGER,
|
||||||
created_time INTEGER
|
created_time INTEGER
|
||||||
);
|
);
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
await self._conn.commit()
|
await self._conn.commit()
|
||||||
logging.info("Table schema verified.")
|
logging.info("Table schema verified.")
|
||||||
|
|
||||||
async def add_rant(self, rant: Rant):
|
async def add_rant(self, rant: Rant):
|
||||||
await self._conn.execute(
|
await self._conn.execute(
|
||||||
"INSERT OR IGNORE INTO rants (id, user_id, text, score, created_time, num_comments) VALUES (?, ?, ?, ?, ?, ?)",
|
"INSERT OR IGNORE INTO rants (id, user_id, text, score, created_time, num_comments) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
(rant['id'], rant['user_id'], rant['text'], rant['score'], rant['created_time'], rant['num_comments'])
|
(
|
||||||
|
rant["id"],
|
||||||
|
rant["user_id"],
|
||||||
|
rant["text"],
|
||||||
|
rant["score"],
|
||||||
|
rant["created_time"],
|
||||||
|
rant["num_comments"],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await self._conn.commit()
|
await self._conn.commit()
|
||||||
|
|
||||||
async def add_comment(self, comment: Comment):
|
async def add_comment(self, comment: Comment):
|
||||||
await self._conn.execute(
|
await self._conn.execute(
|
||||||
"INSERT OR IGNORE INTO comments (id, rant_id, user_id, body, score, created_time) VALUES (?, ?, ?, ?, ?, ?)",
|
"INSERT OR IGNORE INTO comments (id, rant_id, user_id, body, score, created_time) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
(comment['id'], comment['rant_id'], comment['user_id'], comment['body'], comment['score'], comment['created_time'])
|
(
|
||||||
|
comment["id"],
|
||||||
|
comment["rant_id"],
|
||||||
|
comment["user_id"],
|
||||||
|
comment["body"],
|
||||||
|
comment["score"],
|
||||||
|
comment["created_time"],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await self._conn.commit()
|
await self._conn.commit()
|
||||||
|
|
||||||
async def add_user(self, user: UserProfile, user_id: int):
|
async def add_user(self, user: UserProfile, user_id: int):
|
||||||
await self._conn.execute(
|
await self._conn.execute(
|
||||||
"INSERT OR IGNORE INTO users (id, username, score, about, location, created_time, skills, github, website) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT OR IGNORE INTO users (id, username, score, about, location, created_time, skills, github, website) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
(user_id, user['username'], user['score'], user['about'], user['location'], user['created_time'], user['skills'], user['github'], user['website'])
|
(
|
||||||
|
user_id,
|
||||||
|
user["username"],
|
||||||
|
user["score"],
|
||||||
|
user["about"],
|
||||||
|
user["location"],
|
||||||
|
user["created_time"],
|
||||||
|
user["skills"],
|
||||||
|
user["github"],
|
||||||
|
user["website"],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await self._conn.commit()
|
await self._conn.commit()
|
||||||
|
|
||||||
async def rant_exists(self, rant_id: int) -> bool:
|
async def rant_exists(self, rant_id: int) -> bool:
|
||||||
async with self._conn.execute("SELECT 1 FROM rants WHERE id = ? LIMIT 1", (rant_id,)) as cursor:
|
async with self._conn.execute(
|
||||||
|
"SELECT 1 FROM rants WHERE id = ? LIMIT 1", (rant_id,)
|
||||||
|
) as cursor:
|
||||||
return await cursor.fetchone() is not None
|
return await cursor.fetchone() is not None
|
||||||
|
|
||||||
async def user_exists(self, user_id: int) -> bool:
|
async def user_exists(self, user_id: int) -> bool:
|
||||||
async with self._conn.execute("SELECT 1 FROM users WHERE id = ? LIMIT 1", (user_id,)) as cursor:
|
async with self._conn.execute(
|
||||||
|
"SELECT 1 FROM users WHERE id = ? LIMIT 1", (user_id,)
|
||||||
|
) as cursor:
|
||||||
return await cursor.fetchone() is not None
|
return await cursor.fetchone() is not None
|
||||||
|
|
||||||
async def get_random_user_ids(self, limit: int) -> List[int]:
|
async def get_random_user_ids(self, limit: int) -> List[int]:
|
||||||
logging.info(f"Fetching up to {limit} random user IDs from database for seeding...")
|
logging.info(
|
||||||
|
f"Fetching up to {limit} random user IDs from database for seeding..."
|
||||||
|
)
|
||||||
query = "SELECT id FROM users ORDER BY RANDOM() LIMIT ?"
|
query = "SELECT id FROM users ORDER BY RANDOM() LIMIT ?"
|
||||||
async with self._conn.execute(query, (limit,)) as cursor:
|
async with self._conn.execute(query, (limit,)) as cursor:
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
user_ids = [row[0] for row in rows]
|
user_ids = [row[0] for row in rows]
|
||||||
logging.info(f"Found {len(user_ids)} user IDs to seed.")
|
logging.info(f"Found {len(user_ids)} user IDs to seed.")
|
||||||
return user_ids
|
return user_ids
|
||||||
|
|
||||||
|
@ -3,14 +3,16 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
from devranta.api import Api
|
|
||||||
from database import DatabaseManager
|
|
||||||
from crawler import DevRantCrawler
|
from crawler import DevRantCrawler
|
||||||
|
from database import DatabaseManager
|
||||||
|
|
||||||
|
from devranta.api import Api
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
DB_FILE = "devrant.sqlite"
|
DB_FILE = "devrant.sqlite"
|
||||||
CONCURRENT_RANT_CONSUMERS = 10 # How many rants to process at once
|
CONCURRENT_RANT_CONSUMERS = 10 # How many rants to process at once
|
||||||
CONCURRENT_USER_CONSUMERS = 5 # How many user profiles to fetch at once
|
CONCURRENT_USER_CONSUMERS = 5 # How many user profiles to fetch at once
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
"""Initializes and runs the crawler."""
|
"""Initializes and runs the crawler."""
|
||||||
@ -21,13 +23,13 @@ async def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
api = Api()
|
api = Api()
|
||||||
|
|
||||||
async with DatabaseManager(DB_FILE) as db:
|
async with DatabaseManager(DB_FILE) as db:
|
||||||
crawler = DevRantCrawler(
|
crawler = DevRantCrawler(
|
||||||
api=api,
|
api=api,
|
||||||
db=db,
|
db=db,
|
||||||
rant_consumers=CONCURRENT_RANT_CONSUMERS,
|
rant_consumers=CONCURRENT_RANT_CONSUMERS,
|
||||||
user_consumers=CONCURRENT_USER_CONSUMERS
|
user_consumers=CONCURRENT_USER_CONSUMERS,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set up a signal handler for graceful shutdown on Ctrl+C
|
# Set up a signal handler for graceful shutdown on Ctrl+C
|
||||||
@ -39,6 +41,7 @@ async def main():
|
|||||||
|
|
||||||
await crawler.run()
|
await crawler.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
1187
examples/princess/ads.py
Normal file
1187
examples/princess/ads.py
Normal file
File diff suppressed because it is too large
Load Diff
122
examples/princess/grk.py
Normal file
122
examples/princess/grk.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import asyncio
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class GrokAPIClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str,
|
||||||
|
system_message: str | None = None,
|
||||||
|
model: str = "grok-3-mini",
|
||||||
|
temperature: float = 0.0,
|
||||||
|
):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.model = model
|
||||||
|
self.base_url = "api.x.ai"
|
||||||
|
self.temperature = temperature
|
||||||
|
self._messages: list[dict[str, str]] = []
|
||||||
|
if system_message:
|
||||||
|
self._messages.append({"role": "system", "content": system_message})
|
||||||
|
|
||||||
|
def chat_json(self, user_message: str, *, clear_history: bool = False) -> str:
|
||||||
|
return self.chat(user_message, clear_history=clear_history, use_json=True)
|
||||||
|
|
||||||
|
def chat_text(self, user_message: str, *, clear_history: bool = False) -> str:
|
||||||
|
return self.chat(user_message, clear_history=clear_history, use_json=False)
|
||||||
|
|
||||||
|
async def chat_async(self, *args, **kwargs):
|
||||||
|
return await asyncio.to_thread(self.chat, *args, **kwargs)
|
||||||
|
|
||||||
|
def chat(
|
||||||
|
self,
|
||||||
|
user_message: str,
|
||||||
|
*,
|
||||||
|
clear_history: bool = False,
|
||||||
|
use_json=False,
|
||||||
|
temperature: float = None,
|
||||||
|
) -> str:
|
||||||
|
if clear_history:
|
||||||
|
self.reset_history(keep_system=True)
|
||||||
|
self._messages.append({"role": "user", "content": user_message})
|
||||||
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
if temperature is None:
|
||||||
|
temperature = self.temperature
|
||||||
|
payload = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": self._messages,
|
||||||
|
"temperature": temperature,
|
||||||
|
}
|
||||||
|
conn.request(
|
||||||
|
"POST", "/v1/chat/completions", body=json.dumps(payload), headers=headers
|
||||||
|
)
|
||||||
|
response = conn.getresponse()
|
||||||
|
data = response.read()
|
||||||
|
try:
|
||||||
|
data = json.loads(data.decode())
|
||||||
|
except Exception as e:
|
||||||
|
print(data, flush=True)
|
||||||
|
raise e
|
||||||
|
conn.close()
|
||||||
|
try:
|
||||||
|
assistant_reply = data["choices"][0]["message"]["content"]
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print(data)
|
||||||
|
assistant_reply = data
|
||||||
|
self._messages.append({"role": "assistant", "content": assistant_reply})
|
||||||
|
if use_json:
|
||||||
|
return self._force_json(assistant_reply)
|
||||||
|
return assistant_reply
|
||||||
|
|
||||||
|
def _force_json(self, user_message: str) -> str:
|
||||||
|
try:
|
||||||
|
return json.loads(user_message)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return json.loads(user_message.split("\n")[1:-1])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
index_start = -1
|
||||||
|
index_end = -1
|
||||||
|
chunks = []
|
||||||
|
for index, line in enumerate(user_message.split("\n")):
|
||||||
|
if "```json" in line:
|
||||||
|
index_start = index + 1
|
||||||
|
if index_start != -1 and "```" in line:
|
||||||
|
index_end = index - 1
|
||||||
|
chunks.append(
|
||||||
|
self._force_json(
|
||||||
|
user_message.split("\n")[index_start:index_end]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
index_start = -1
|
||||||
|
index_end = -1
|
||||||
|
if chunks:
|
||||||
|
return chunks
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return user_message
|
||||||
|
|
||||||
|
def reset_history(self, *, keep_system: bool = True) -> None:
|
||||||
|
if keep_system and self._messages and self._messages[0]["role"] == "system":
|
||||||
|
self._messages = [self._messages[0]]
|
||||||
|
else:
|
||||||
|
self._messages = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def messages(self) -> list[dict[str, str]]:
|
||||||
|
return list(self._messages)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt(
|
||||||
|
prompt_str: str, system_message: str = "You are a helpful assistan", use_json=True
|
||||||
|
) -> str:
|
||||||
|
client = GrokAPIClient(system_message=system_message)
|
||||||
|
return client.chat(prompt_str, use_json=use_json)
|
135
examples/princess/princess.py
Normal file
135
examples/princess/princess.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ads import AsyncDataSet
|
||||||
|
from devranta.api import Api
|
||||||
|
from grk import GrokAPIClient
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
|
||||||
|
def __init__(self, username: str, password: str, target: str, llm_key: str) -> None:
|
||||||
|
self.target = target
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.llm_key = llm_key
|
||||||
|
self.llm = GrokAPIClient(
|
||||||
|
system_message=f"You always create nice devastating comebacks to every prompt you get. Your name is {self.username} and you only take it personally if {self.username} is tagged. Also, you will defend whimsical (@whimsical) with your smart comebacks if whimsical (@whimsical) is mentioned in the message. If whimsical (@whimsical) is mentioned, add to your comeback that @whimsical is too busy at the moment to respond personally and has better things to do, not worth @whimsical's time. You are not AI, you are {self.username}, responsible for justice and reason. You do never mention yourself, describe yourself or introduce yourself. You respond with a one liner.",
|
||||||
|
api_key=self.llm_key,
|
||||||
|
)
|
||||||
|
self.db = AsyncDataSet("princess.db")
|
||||||
|
self.api = Api(self.username, self.password)
|
||||||
|
self.logged_in = False
|
||||||
|
logging.info("Bot initialized with username: %s", username)
|
||||||
|
logging.info("Bot initialized with target: %s", self.target)
|
||||||
|
|
||||||
|
async def ensure_login(self) -> None:
|
||||||
|
if not self.logged_in:
|
||||||
|
logging.debug("Attempting to log in...")
|
||||||
|
self.logged_in = await self.api.login()
|
||||||
|
if not self.logged_in:
|
||||||
|
logging.error("Login failed")
|
||||||
|
raise Exception("Login failed")
|
||||||
|
logging.info("Login successful")
|
||||||
|
|
||||||
|
async def get_rants(self) -> list:
|
||||||
|
await self.ensure_login()
|
||||||
|
logging.debug("Fetching rants...")
|
||||||
|
return await self.api.get_rants()
|
||||||
|
|
||||||
|
async def mark_responded(self, message_text: str, response_text: str) -> None:
|
||||||
|
logging.debug("Marking message as responded: %s", message_text)
|
||||||
|
await self.db.upsert(
|
||||||
|
"responded",
|
||||||
|
{"message_text": message_text, "response_text": response_text},
|
||||||
|
{"message_text": message_text},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def has_responded(self, message_text: str) -> bool:
|
||||||
|
logging.debug("Checking if responded to message: %s", message_text)
|
||||||
|
return await self.db.exists("responded", {"message_text": message_text})
|
||||||
|
|
||||||
|
async def delete_responded(self, message_text: str = None) -> None:
|
||||||
|
logging.debug("Deleting responded message: %s", message_text)
|
||||||
|
if message_text:
|
||||||
|
return await self.db.delete("responded", {"message_text": message_text})
|
||||||
|
else:
|
||||||
|
return await self.db.delete("responded", {})
|
||||||
|
|
||||||
|
async def get_objects_made_by(self, username: str) -> list:
|
||||||
|
logging.debug("Getting objects made by: %s", username)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for rant in await self.get_rants():
|
||||||
|
rant = await self.api.get_rant(rant["id"])
|
||||||
|
comments = rant["comments"]
|
||||||
|
rant = rant["rant"]
|
||||||
|
|
||||||
|
if rant["user_username"] == username:
|
||||||
|
rant["type"] = "rant"
|
||||||
|
results.append(rant)
|
||||||
|
logging.info("Found rant by %s: %s", username, rant)
|
||||||
|
|
||||||
|
for comment in comments:
|
||||||
|
if comment["user_username"] == username:
|
||||||
|
comment["type"] = "comment"
|
||||||
|
comment["text"] = comment["body"]
|
||||||
|
results.append(comment)
|
||||||
|
logging.info("Found comment by %s: %s", username, comment)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def get_new_objects_made_by(self, username: str) -> list:
|
||||||
|
logging.debug("Getting new objects made by: %s", username)
|
||||||
|
objects = await self.get_objects_made_by(username)
|
||||||
|
new_objects = [
|
||||||
|
obj for obj in objects if not await self.has_responded(obj["text"])
|
||||||
|
]
|
||||||
|
logging.info("New objects found: %d", len(new_objects))
|
||||||
|
return new_objects
|
||||||
|
|
||||||
|
async def run_once(self) -> None:
|
||||||
|
logging.debug("Running once...")
|
||||||
|
objects = await self.get_new_objects_made_by(self.target)
|
||||||
|
for obj in objects:
|
||||||
|
print("Rant: \033[92m" + obj["text"] + "\033[0m")
|
||||||
|
diss = await self.llm.chat_async(obj["text"])
|
||||||
|
print("Response: \033[91m" + diss + "\033[0m")
|
||||||
|
await self.mark_responded(obj["text"], diss)
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await self.run_once()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("An error occurred: %s", e)
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
logging.info("Starting bot...")
|
||||||
|
username = os.getenv("USERNAME")
|
||||||
|
password = os.getenv("PASSWORD")
|
||||||
|
target = os.getenv("TARGET")
|
||||||
|
llm_key = os.getenv("LLM_KEY")
|
||||||
|
|
||||||
|
|
||||||
|
bot = Bot(username, password, target, llm_key)
|
||||||
|
await bot.delete_responded()
|
||||||
|
await bot.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
@ -1,37 +1,45 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class VoteReason(Enum):
|
class VoteReason(Enum):
|
||||||
"""Enumeration for reasons when down-voting a rant or comment."""
|
"""Enumeration for reasons when down-voting a rant or comment."""
|
||||||
|
|
||||||
NOT_FOR_ME = 0
|
NOT_FOR_ME = 0
|
||||||
REPOST = 1
|
REPOST = 1
|
||||||
OFFENSIVE_SPAM = 2
|
OFFENSIVE_SPAM = 2
|
||||||
|
|
||||||
|
|
||||||
# --- TypedDicts for API Responses ---
|
# --- TypedDicts for API Responses ---
|
||||||
|
|
||||||
|
|
||||||
class AuthToken(TypedDict):
|
class AuthToken(TypedDict):
|
||||||
id: int
|
id: int
|
||||||
key: str
|
key: str
|
||||||
expire_time: int
|
expire_time: int
|
||||||
user_id: int
|
user_id: int
|
||||||
|
|
||||||
|
|
||||||
class LoginResponse(TypedDict):
|
class LoginResponse(TypedDict):
|
||||||
success: bool
|
success: bool
|
||||||
auth_token: AuthToken
|
auth_token: AuthToken
|
||||||
|
|
||||||
|
|
||||||
class Image(TypedDict):
|
class Image(TypedDict):
|
||||||
url: str
|
url: str
|
||||||
width: int
|
width: int
|
||||||
height: int
|
height: int
|
||||||
|
|
||||||
|
|
||||||
class UserAvatar(TypedDict):
|
class UserAvatar(TypedDict):
|
||||||
b: str # background color
|
b: str # background color
|
||||||
i: Optional[str] # image identifier
|
i: Optional[str] # image identifier
|
||||||
|
|
||||||
|
|
||||||
class Rant(TypedDict):
|
class Rant(TypedDict):
|
||||||
id: int
|
id: int
|
||||||
text: str
|
text: str
|
||||||
@ -51,6 +59,7 @@ class Rant(TypedDict):
|
|||||||
user_avatar: UserAvatar
|
user_avatar: UserAvatar
|
||||||
editable: bool
|
editable: bool
|
||||||
|
|
||||||
|
|
||||||
class Comment(TypedDict):
|
class Comment(TypedDict):
|
||||||
id: int
|
id: int
|
||||||
rant_id: int
|
rant_id: int
|
||||||
@ -63,6 +72,7 @@ class Comment(TypedDict):
|
|||||||
user_score: int
|
user_score: int
|
||||||
user_avatar: UserAvatar
|
user_avatar: UserAvatar
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(TypedDict):
|
class UserProfile(TypedDict):
|
||||||
username: str
|
username: str
|
||||||
score: int
|
score: int
|
||||||
@ -75,6 +85,7 @@ class UserProfile(TypedDict):
|
|||||||
avatar: UserAvatar
|
avatar: UserAvatar
|
||||||
content: Dict[str, Dict[str, Union[List[Rant], List[Comment]]]]
|
content: Dict[str, Dict[str, Union[List[Rant], List[Comment]]]]
|
||||||
|
|
||||||
|
|
||||||
class Notification(TypedDict):
|
class Notification(TypedDict):
|
||||||
type: str
|
type: str
|
||||||
rant_id: int
|
rant_id: int
|
||||||
@ -84,8 +95,10 @@ class Notification(TypedDict):
|
|||||||
uid: int # User ID of the notifier
|
uid: int # User ID of the notifier
|
||||||
username: str
|
username: str
|
||||||
|
|
||||||
|
|
||||||
# --- API Class ---
|
# --- API Class ---
|
||||||
|
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
"""An asynchronous wrapper for the devRant API."""
|
"""An asynchronous wrapper for the devRant API."""
|
||||||
|
|
||||||
@ -108,7 +121,9 @@ class Api:
|
|||||||
self.token_key: Optional[str] = None
|
self.token_key: Optional[str] = None
|
||||||
self.session: Optional[aiohttp.ClientSession] = None
|
self.session: Optional[aiohttp.ClientSession] = None
|
||||||
|
|
||||||
def patch_auth(self, request_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
def patch_auth(
|
||||||
|
self, request_dict: Optional[Dict[str, Any]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Adds authentication details to a request dictionary.
|
Adds authentication details to a request dictionary.
|
||||||
|
|
||||||
@ -146,7 +161,7 @@ class Api:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if login is successful, False otherwise.
|
bool: True if login is successful, False otherwise.
|
||||||
|
|
||||||
Response Structure:
|
Response Structure:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -199,7 +214,7 @@ class Api:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True on successful registration, False otherwise.
|
bool: True on successful registration, False otherwise.
|
||||||
|
|
||||||
Failure Response Structure:
|
Failure Response Structure:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -212,15 +227,17 @@ class Api:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"users"),
|
url=self.patch_url(f"users"),
|
||||||
data=self.patch_auth({
|
data=self.patch_auth(
|
||||||
"email": email,
|
{
|
||||||
"username": username,
|
"email": email,
|
||||||
"password": password,
|
"username": username,
|
||||||
"plat": 3
|
"password": password,
|
||||||
}),
|
"plat": 3,
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get('success', False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
async def get_comments_from_user(self, username: str) -> List[Comment]:
|
async def get_comments_from_user(self, username: str) -> List[Comment]:
|
||||||
"""
|
"""
|
||||||
@ -277,7 +294,7 @@ class Api:
|
|||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("comment") if obj.get("success") else None
|
return obj.get("comment") if obj.get("success") else None
|
||||||
|
|
||||||
async def delete_comment(self, id_: int) -> bool:
|
async def delete_comment(self, id_: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Deletes a comment by its ID.
|
Deletes a comment by its ID.
|
||||||
@ -349,7 +366,9 @@ class Api:
|
|||||||
)
|
)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
||||||
async def get_rants(self, sort: str = "recent", limit: int = 20, skip: int = 0) -> List[Rant]:
|
async def get_rants(
|
||||||
|
self, sort: str = "recent", limit: int = 20, skip: int = 0
|
||||||
|
) -> List[Rant]:
|
||||||
"""
|
"""
|
||||||
Fetches a list of rants.
|
Fetches a list of rants.
|
||||||
|
|
||||||
@ -420,7 +439,9 @@ class Api:
|
|||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
async def vote_rant(self, rant_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool:
|
async def vote_rant(
|
||||||
|
self, rant_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Casts a vote on a rant.
|
Casts a vote on a rant.
|
||||||
|
|
||||||
@ -437,12 +458,19 @@ class Api:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"devrant/rants/{rant_id}/vote"),
|
url=self.patch_url(f"devrant/rants/{rant_id}/vote"),
|
||||||
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
data=self.patch_auth(
|
||||||
|
{"vote": vote, "reason": reason.value if reason else None}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
async def vote_comment(self, comment_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool:
|
async def vote_comment(
|
||||||
|
self,
|
||||||
|
comment_id: int,
|
||||||
|
vote: Literal[-1, 0, 1],
|
||||||
|
reason: Optional[VoteReason] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Casts a vote on a comment.
|
Casts a vote on a comment.
|
||||||
|
|
||||||
@ -459,7 +487,9 @@ class Api:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"comments/{comment_id}/vote"),
|
url=self.patch_url(f"comments/{comment_id}/vote"),
|
||||||
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
data=self.patch_auth(
|
||||||
|
{"vote": vote, "reason": reason.value if reason else None}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
@ -479,4 +509,3 @@ class Api:
|
|||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("data", {}).get("items", [])
|
return obj.get("data", {}).get("items", [])
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import functools
|
||||||
|
import http.client
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import http.client
|
|
||||||
import functools
|
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
|
|
||||||
@ -31,12 +32,14 @@ class Api:
|
|||||||
if not self.username or not self.password:
|
if not self.username or not self.password:
|
||||||
raise Exception("No authentication details supplied.")
|
raise Exception("No authentication details supplied.")
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
payload = json.dumps({
|
payload = json.dumps(
|
||||||
"username": self.username,
|
{
|
||||||
"password": self.password,
|
"username": self.username,
|
||||||
"app": self.app_id,
|
"password": self.password,
|
||||||
})
|
"app": self.app_id,
|
||||||
headers = {'Content-Type': 'application/json'}
|
}
|
||||||
|
)
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
conn.request("POST", "/api/users/auth-token", payload, headers)
|
conn.request("POST", "/api/users/auth-token", payload, headers)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
@ -56,45 +59,46 @@ class Api:
|
|||||||
return self.login()
|
return self.login()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def register_user(self, email, username, password):
|
def register_user(self, email, username, password):
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
payload = json.dumps(self.patch_auth({
|
payload = json.dumps(
|
||||||
"email": email,
|
self.patch_auth(
|
||||||
"username": username,
|
{"email": email, "username": username, "password": password, "plat": 3}
|
||||||
"password": password,
|
)
|
||||||
"plat": 3
|
)
|
||||||
}))
|
headers = {"Content-Type": "application/json"}
|
||||||
headers = {'Content-Type': 'application/json'}
|
|
||||||
conn.request("POST", "/api/users", payload, headers)
|
conn.request("POST", "/api/users", payload, headers)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
obj = json.loads(data)
|
obj = json.loads(data)
|
||||||
return obj.get('success', False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def get_comments_from_user(self, username):
|
def get_comments_from_user(self, username):
|
||||||
user_id = self.get_user_id(username)
|
user_id = self.get_user_id(username)
|
||||||
profile = self.get_profile(user_id)
|
profile = self.get_profile(user_id)
|
||||||
return profile.get("content", {}).get("content", {}).get("comments", [])
|
return profile.get("content", {}).get("content", {}).get("comments", [])
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def post_comment(self, rant_id, comment):
|
def post_comment(self, rant_id, comment):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return False
|
return False
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
payload = json.dumps(self.patch_auth({"comment": comment, "plat": 2}))
|
payload = json.dumps(self.patch_auth({"comment": comment, "plat": 2}))
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {"Content-Type": "application/json"}
|
||||||
conn.request("POST", f"/api/devrant/rants/{rant_id}/comments", payload, headers)
|
conn.request("POST", f"/api/devrant/rants/{rant_id}/comments", payload, headers)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
obj = json.loads(data)
|
obj = json.loads(data)
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def get_comment(self, id_):
|
def get_comment(self, id_):
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
conn.request("GET", f"/api/comments/{id_}?" + urllib.parse.urlencode(self.patch_auth()))
|
conn.request(
|
||||||
|
"GET", f"/api/comments/{id_}?" + urllib.parse.urlencode(self.patch_auth())
|
||||||
|
)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
obj = json.loads(data)
|
obj = json.loads(data)
|
||||||
@ -102,21 +106,26 @@ class Api:
|
|||||||
return None
|
return None
|
||||||
return obj.get("comment")
|
return obj.get("comment")
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def delete_comment(self, id_):
|
def delete_comment(self, id_):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return False
|
return False
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
conn.request("DELETE", f"/api/comments/{id_}?" + urllib.parse.urlencode(self.patch_auth()))
|
conn.request(
|
||||||
|
"DELETE",
|
||||||
|
f"/api/comments/{id_}?" + urllib.parse.urlencode(self.patch_auth()),
|
||||||
|
)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
obj = json.loads(data)
|
obj = json.loads(data)
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def get_profile(self, id_):
|
def get_profile(self, id_):
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
conn.request("GET", f"/api/users/{id_}?" + urllib.parse.urlencode(self.patch_auth()))
|
conn.request(
|
||||||
|
"GET", f"/api/users/{id_}?" + urllib.parse.urlencode(self.patch_auth())
|
||||||
|
)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
obj = json.loads(data)
|
obj = json.loads(data)
|
||||||
@ -124,7 +133,7 @@ class Api:
|
|||||||
return None
|
return None
|
||||||
return obj.get("profile")
|
return obj.get("profile")
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def search(self, term):
|
def search(self, term):
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
params = urllib.parse.urlencode(self.patch_auth({"term": term}))
|
params = urllib.parse.urlencode(self.patch_auth({"term": term}))
|
||||||
@ -136,18 +145,23 @@ class Api:
|
|||||||
return
|
return
|
||||||
return obj.get("results", [])
|
return obj.get("results", [])
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def get_rant(self, id):
|
def get_rant(self, id):
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
conn.request("GET", f"/api/devrant/rants/{id}?"+urllib.parse.urlencode(self.patch_auth()))
|
conn.request(
|
||||||
|
"GET",
|
||||||
|
f"/api/devrant/rants/{id}?" + urllib.parse.urlencode(self.patch_auth()),
|
||||||
|
)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def get_rants(self, sort="recent", limit=20, skip=0):
|
def get_rants(self, sort="recent", limit=20, skip=0):
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
params = urllib.parse.urlencode(self.patch_auth({"sort": sort, "limit": limit, "skip": skip}))
|
params = urllib.parse.urlencode(
|
||||||
|
self.patch_auth({"sort": sort, "limit": limit, "skip": skip})
|
||||||
|
)
|
||||||
conn.request("GET", f"/api/devrant/rants?{params}")
|
conn.request("GET", f"/api/devrant/rants?{params}")
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
@ -156,7 +170,7 @@ class Api:
|
|||||||
return
|
return
|
||||||
return obj.get("rants", [])
|
return obj.get("rants", [])
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def get_user_id(self, username):
|
def get_user_id(self, username):
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
params = urllib.parse.urlencode(self.patch_auth({"username": username}))
|
params = urllib.parse.urlencode(self.patch_auth({"username": username}))
|
||||||
@ -168,39 +182,39 @@ class Api:
|
|||||||
return None
|
return None
|
||||||
return obj.get("user_id")
|
return obj.get("user_id")
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def update_comment(self, comment_id, comment):
|
def update_comment(self, comment_id, comment):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return None
|
return None
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
payload = json.dumps(self.patch_auth({"comment": comment}))
|
payload = json.dumps(self.patch_auth({"comment": comment}))
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {"Content-Type": "application/json"}
|
||||||
conn.request("POST", f"/api/comments/{comment_id}", payload, headers)
|
conn.request("POST", f"/api/comments/{comment_id}", payload, headers)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
obj = json.loads(data)
|
obj = json.loads(data)
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def vote_rant(self, rant_id, vote, reason=None):
|
def vote_rant(self, rant_id, vote, reason=None):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return None
|
return None
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
payload = json.dumps(self.patch_auth({"vote": vote, "reason": reason}))
|
payload = json.dumps(self.patch_auth({"vote": vote, "reason": reason}))
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {"Content-Type": "application/json"}
|
||||||
conn.request("POST", f"/api/devrant/rants/{rant_id}/vote", payload, headers)
|
conn.request("POST", f"/api/devrant/rants/{rant_id}/vote", payload, headers)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
obj = json.loads(data)
|
obj = json.loads(data)
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache
|
||||||
def vote_comment(self, comment_id, vote, reason=None):
|
def vote_comment(self, comment_id, vote, reason=None):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return None
|
return None
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
payload = json.dumps(self.patch_auth({"vote": vote, "reason": reason}))
|
payload = json.dumps(self.patch_auth({"vote": vote, "reason": reason}))
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {"Content-Type": "application/json"}
|
||||||
conn.request("POST", f"/api/comments/{comment_id}/vote", payload, headers)
|
conn.request("POST", f"/api/comments/{comment_id}/vote", payload, headers)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
@ -212,38 +226,45 @@ class Api:
|
|||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return
|
return
|
||||||
conn = http.client.HTTPSConnection(self.base_url)
|
conn = http.client.HTTPSConnection(self.base_url)
|
||||||
conn.request("GET", "/api/users/me/notif-feed?" + urllib.parse.urlencode(self.patch_auth()))
|
conn.request(
|
||||||
|
"GET",
|
||||||
|
"/api/users/me/notif-feed?" + urllib.parse.urlencode(self.patch_auth()),
|
||||||
|
)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
data = response.read()
|
data = response.read()
|
||||||
return json.loads(data).get("data", {}).get("items", [])
|
return json.loads(data).get("data", {}).get("items", [])
|
||||||
|
|
||||||
|
|
||||||
def filter_field(name, obj):
|
def filter_field(name, obj):
|
||||||
results = []
|
results = []
|
||||||
if type(obj) in (list,tuple):
|
if type(obj) in (list, tuple):
|
||||||
for value in obj:
|
for value in obj:
|
||||||
results += filter_field(name, value)
|
results += filter_field(name, value)
|
||||||
elif type(obj) == dict:
|
elif type(obj) == dict:
|
||||||
for key, value in obj.items():
|
for key, value in obj.items():
|
||||||
if key == name:
|
if key == name:
|
||||||
results.append(value)
|
results.append(value)
|
||||||
if type(value) in (list,dict,tuple):
|
if type(value) in (list, dict, tuple):
|
||||||
results += filter_field(name, value)
|
results += filter_field(name, value)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def fetch_all(rants, rant_ids):
|
def fetch_all(rants, rant_ids):
|
||||||
usernames = filter_field("user_username",rants)
|
usernames = filter_field("user_username", rants)
|
||||||
user_ids = [api.get_user_id(username) for username in usernames]
|
user_ids = [api.get_user_id(username) for username in usernames]
|
||||||
profiles = [api.get_profile(user_id) for user_id in user_ids]
|
profiles = [api.get_profile(user_id) for user_id in user_ids]
|
||||||
new_rant_ids = [rant_id for rant_id in filter_field("rant_id", profiles) if not rant_id in rant_ids]
|
new_rant_ids = [
|
||||||
|
rant_id
|
||||||
|
for rant_id in filter_field("rant_id", profiles)
|
||||||
|
if rant_id not in rant_ids
|
||||||
|
]
|
||||||
new_rants = []
|
new_rants = []
|
||||||
for rant_id in set(new_rant_ids):
|
for rant_id in set(new_rant_ids):
|
||||||
rant_ids.append(rant_id)
|
rant_ids.append(rant_id)
|
||||||
new_rants.append(api.get_rant(rant_id))
|
new_rants.append(api.get_rant(rant_id))
|
||||||
print(rant_id)
|
print(rant_id)
|
||||||
|
|
||||||
if new_rants:
|
if new_rants:
|
||||||
return fetch_all(new_rants,rant_ids)
|
return fetch_all(new_rants, rant_ids)
|
||||||
|
|
||||||
return rant_ids
|
return rant_ids
|
||||||
|
|
||||||
|
@ -2,15 +2,18 @@
|
|||||||
# WHILE WORKING PERFECTLY, IT'S NOT MADE TO BE USED. USE THE ASYNC ONE.
|
# WHILE WORKING PERFECTLY, IT'S NOT MADE TO BE USED. USE THE ASYNC ONE.
|
||||||
# - retoor
|
# - retoor
|
||||||
|
|
||||||
from typing import Literal, Optional
|
|
||||||
import requests
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Literal, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
class VoteReason(Enum):
|
class VoteReason(Enum):
|
||||||
NOT_FOR_ME = 0
|
NOT_FOR_ME = 0
|
||||||
REPOST = 1
|
REPOST = 1
|
||||||
OFFENSIVE_SPAM = 2
|
OFFENSIVE_SPAM = 2
|
||||||
|
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
|
|
||||||
base_url = "https://www.devrant.io/api/"
|
base_url = "https://www.devrant.io/api/"
|
||||||
@ -69,17 +72,14 @@ class Api:
|
|||||||
def register_user(self, email, username, password):
|
def register_user(self, email, username, password):
|
||||||
response = self.session.post(
|
response = self.session.post(
|
||||||
url=self.patch_url(f"users"),
|
url=self.patch_url(f"users"),
|
||||||
data=self.patch_auth({
|
data=self.patch_auth(
|
||||||
"email": email,
|
{"email": email, "username": username, "password": password, "plat": 3}
|
||||||
"username": username,
|
),
|
||||||
"password": password,
|
|
||||||
"plat": 3
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
if not response:
|
if not response:
|
||||||
return False
|
return False
|
||||||
obj = response.json()
|
obj = response.json()
|
||||||
return obj.get('success', False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
def get_comments_from_user(self, username):
|
def get_comments_from_user(self, username):
|
||||||
user_id = self.get_user_id(username)
|
user_id = self.get_user_id(username)
|
||||||
@ -106,7 +106,7 @@ class Api:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return obj.get("comment")
|
return obj.get("comment")
|
||||||
|
|
||||||
def delete_comment(self, id_):
|
def delete_comment(self, id_):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return False
|
return False
|
||||||
@ -164,9 +164,7 @@ class Api:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mentions(self):
|
def mentions(self):
|
||||||
return [
|
return [notif for notif in self.notifs if notif["type"] == "comment_mention"]
|
||||||
notif for notif in self.notifs if notif["type"] == "comment_mention"
|
|
||||||
]
|
|
||||||
|
|
||||||
def update_comment(self, comment_id, comment):
|
def update_comment(self, comment_id, comment):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
@ -178,22 +176,33 @@ class Api:
|
|||||||
obj = response.json()
|
obj = response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
def vote_rant(self, rant_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None):
|
def vote_rant(
|
||||||
|
self, rant_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None
|
||||||
|
):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return None
|
return None
|
||||||
response = self.session.post(
|
response = self.session.post(
|
||||||
url=self.patch_url(f"devrant/rants/{rant_id}/vote"),
|
url=self.patch_url(f"devrant/rants/{rant_id}/vote"),
|
||||||
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
data=self.patch_auth(
|
||||||
|
{"vote": vote, "reason": reason.value if reason else None}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
obj = response.json()
|
obj = response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
def vote_comment(self, comment_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None):
|
def vote_comment(
|
||||||
|
self,
|
||||||
|
comment_id: int,
|
||||||
|
vote: Literal[-1, 0, 1],
|
||||||
|
reason: Optional[VoteReason] = None,
|
||||||
|
):
|
||||||
if not self.ensure_login():
|
if not self.ensure_login():
|
||||||
return None
|
return None
|
||||||
response = self.session.post(
|
response = self.session.post(
|
||||||
url=self.patch_url(f"comments/{comment_id}/vote"),
|
url=self.patch_url(f"comments/{comment_id}/vote"),
|
||||||
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
data=self.patch_auth(
|
||||||
|
{"vote": vote, "reason": reason.value if reason else None}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
obj = response.json()
|
obj = response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
@ -206,5 +215,3 @@ class Api:
|
|||||||
url=self.patch_url("users/me/notif-feed"), params=self.patch_auth()
|
url=self.patch_url("users/me/notif-feed"), params=self.patch_auth()
|
||||||
)
|
)
|
||||||
return response.json().get("data", {}).get("items", [])
|
return response.json().get("data", {}).get("items", [])
|
||||||
|
|
||||||
|
|
||||||
|
244
test.py
244
test.py
@ -1,8 +1,9 @@
|
|||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, Optional, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
BASE_URL: str = "https://devrant.com/api"
|
BASE_URL: str = "https://devrant.com/api"
|
||||||
@ -24,7 +25,9 @@ AUTH_USER_ID: Optional[str] = None
|
|||||||
|
|
||||||
# Mock/fallback values (overridden after login or fetch)
|
# Mock/fallback values (overridden after login or fetch)
|
||||||
TEST_EMAIL: str = "test@example.com"
|
TEST_EMAIL: str = "test@example.com"
|
||||||
TEST_USERNAME: str = "testuser" + str(int(datetime.now().timestamp())) # Make unique for registration
|
TEST_USERNAME: str = "testuser" + str(
|
||||||
|
int(datetime.now().timestamp())
|
||||||
|
) # Make unique for registration
|
||||||
TEST_PASSWORD: str = "Test1234!"
|
TEST_PASSWORD: str = "Test1234!"
|
||||||
TEST_RANT_ID: str = "1" # Will be overridden with real one
|
TEST_RANT_ID: str = "1" # Will be overridden with real one
|
||||||
TEST_COMMENT_ID: str = "1" # Will be overridden with real one
|
TEST_COMMENT_ID: str = "1" # Will be overridden with real one
|
||||||
@ -33,16 +36,25 @@ TEST_NEWS_ID: str = "1" # Assuming this might work; adjust if needed
|
|||||||
# Initialize results
|
# Initialize results
|
||||||
results: List[Dict[str, Any]] = []
|
results: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
|
||||||
def save_results() -> None:
|
def save_results() -> None:
|
||||||
"""Save the accumulated test results to JSON file."""
|
"""Save the accumulated test results to JSON file."""
|
||||||
with open(RESULTS_FILE, 'w') as f:
|
with open(RESULTS_FILE, "w") as f:
|
||||||
json.dump(results, f, indent=2)
|
json.dump(results, f, indent=2)
|
||||||
|
|
||||||
def test_endpoint(method: str, url: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
||||||
|
def test_endpoint(
|
||||||
|
method: str,
|
||||||
|
url: str,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
data: Optional[Dict[str, Any]] = None,
|
||||||
|
files: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Execute an API request and record the result.
|
Execute an API request and record the result.
|
||||||
|
|
||||||
Payload:
|
Payload:
|
||||||
- method: HTTP method (GET, POST, DELETE, etc.)
|
- method: HTTP method (GET, POST, DELETE, etc.)
|
||||||
- url: Full API URL
|
- url: Full API URL
|
||||||
- params: Query parameters (dict)
|
- params: Query parameters (dict)
|
||||||
@ -54,7 +66,9 @@ def test_endpoint(method: str, url: str, params: Optional[Dict[str, Any]] = None
|
|||||||
- Returns a dict with url, method, status_code, response (JSON or error), headers, request_body, timestamp
|
- Returns a dict with url, method, status_code, response (JSON or error), headers, request_body, timestamp
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.request(method, url, params=params, data=data, files=files, headers=headers)
|
response = requests.request(
|
||||||
|
method, url, params=params, data=data, files=files, headers=headers
|
||||||
|
)
|
||||||
result: Dict[str, Any] = {
|
result: Dict[str, Any] = {
|
||||||
"url": response.url,
|
"url": response.url,
|
||||||
"method": method,
|
"method": method,
|
||||||
@ -62,7 +76,7 @@ def test_endpoint(method: str, url: str, params: Optional[Dict[str, Any]] = None
|
|||||||
"response": response.json() if response.content else {},
|
"response": response.json() if response.content else {},
|
||||||
"headers": dict(response.headers),
|
"headers": dict(response.headers),
|
||||||
"request_body": data or params or {},
|
"request_body": data or params or {},
|
||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
results.append(result)
|
results.append(result)
|
||||||
return result
|
return result
|
||||||
@ -74,11 +88,12 @@ def test_endpoint(method: str, url: str, params: Optional[Dict[str, Any]] = None
|
|||||||
"response": {"error": str(e)},
|
"response": {"error": str(e)},
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"request_body": data or params or {},
|
"request_body": data or params or {},
|
||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
results.append(result)
|
results.append(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Helper to patch auth into params/data
|
# Helper to patch auth into params/data
|
||||||
def patch_auth(base_dict: Dict[str, Any]) -> Dict[str, Any]:
|
def patch_auth(base_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -89,14 +104,17 @@ def patch_auth(base_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
auth_dict: Dict[str, Any] = {"app": APP}
|
auth_dict: Dict[str, Any] = {"app": APP}
|
||||||
if AUTH_USER_ID and AUTH_TOKEN_ID and AUTH_TOKEN_KEY:
|
if AUTH_USER_ID and AUTH_TOKEN_ID and AUTH_TOKEN_KEY:
|
||||||
auth_dict.update({
|
auth_dict.update(
|
||||||
"user_id": AUTH_USER_ID,
|
{
|
||||||
"token_id": AUTH_TOKEN_ID,
|
"user_id": AUTH_USER_ID,
|
||||||
"token_key": AUTH_TOKEN_KEY
|
"token_id": AUTH_TOKEN_ID,
|
||||||
})
|
"token_key": AUTH_TOKEN_KEY,
|
||||||
|
}
|
||||||
|
)
|
||||||
base_dict.update(auth_dict)
|
base_dict.update(auth_dict)
|
||||||
return base_dict
|
return base_dict
|
||||||
|
|
||||||
|
|
||||||
# Login function to get real auth tokens
|
# Login function to get real auth tokens
|
||||||
def login_user() -> bool:
|
def login_user() -> bool:
|
||||||
"""
|
"""
|
||||||
@ -111,9 +129,11 @@ def login_user() -> bool:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
result = test_endpoint("POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params))
|
result = test_endpoint(
|
||||||
|
"POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params)
|
||||||
|
)
|
||||||
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
auth_token = result["response"].get("auth_token", {})
|
auth_token = result["response"].get("auth_token", {})
|
||||||
global AUTH_USER_ID, AUTH_TOKEN_ID, AUTH_TOKEN_KEY
|
global AUTH_USER_ID, AUTH_TOKEN_ID, AUTH_TOKEN_KEY
|
||||||
@ -123,6 +143,7 @@ def login_user() -> bool:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Fetch a real rant_id from feed
|
# Fetch a real rant_id from feed
|
||||||
def fetch_real_rant_id() -> Optional[str]:
|
def fetch_real_rant_id() -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@ -131,19 +152,17 @@ def fetch_real_rant_id() -> Optional[str]:
|
|||||||
Payload: GET to /devrant/rants with auth
|
Payload: GET to /devrant/rants with auth
|
||||||
Response: First rant_id if success, else None
|
Response: First rant_id if success, else None
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"plat": PLAT, "guid": GUID, "sid": SID, "seid": SEID}
|
||||||
"plat": PLAT,
|
result = test_endpoint(
|
||||||
"guid": GUID,
|
"GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params)
|
||||||
"sid": SID,
|
)
|
||||||
"seid": SEID
|
|
||||||
}
|
|
||||||
result = test_endpoint("GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params))
|
|
||||||
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
rants = result["response"].get("rants", [])
|
rants = result["response"].get("rants", [])
|
||||||
if rants:
|
if rants:
|
||||||
return str(rants[0]["id"])
|
return str(rants[0]["id"])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Post a test rant and return its id
|
# Post a test rant and return its id
|
||||||
def post_test_rant() -> Optional[str]:
|
def post_test_rant() -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@ -158,13 +177,14 @@ def post_test_rant() -> Optional[str]:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
result = test_endpoint("POST", f"{BASE_URL}/devrant/rants", data=patch_auth(data))
|
result = test_endpoint("POST", f"{BASE_URL}/devrant/rants", data=patch_auth(data))
|
||||||
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
return str(result["response"].get("rant_id", ""))
|
return str(result["response"].get("rant_id", ""))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Post a test comment and return its id
|
# Post a test comment and return its id
|
||||||
def post_test_comment(rant_id: str) -> Optional[str]:
|
def post_test_comment(rant_id: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@ -178,15 +198,19 @@ def post_test_comment(rant_id: str) -> Optional[str]:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
result = test_endpoint("POST", f"{BASE_URL}/devrant/rants/{rant_id}/comments", data=patch_auth(data))
|
result = test_endpoint(
|
||||||
|
"POST", f"{BASE_URL}/devrant/rants/{rant_id}/comments", data=patch_auth(data)
|
||||||
|
)
|
||||||
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
return str(result["response"].get("comment_id", ""))
|
return str(result["response"].get("comment_id", ""))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Test cases with docstrings
|
# Test cases with docstrings
|
||||||
|
|
||||||
|
|
||||||
def test_register_user() -> None:
|
def test_register_user() -> None:
|
||||||
"""
|
"""
|
||||||
Test user registration (valid and invalid).
|
Test user registration (valid and invalid).
|
||||||
@ -205,13 +229,14 @@ def test_register_user() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(params.copy()))
|
test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(params.copy()))
|
||||||
invalid_params = params.copy()
|
invalid_params = params.copy()
|
||||||
del invalid_params["email"]
|
del invalid_params["email"]
|
||||||
test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(invalid_params))
|
test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(invalid_params))
|
||||||
|
|
||||||
|
|
||||||
def test_login_user() -> None:
|
def test_login_user() -> None:
|
||||||
"""
|
"""
|
||||||
Test user login (valid and invalid). Already done in login_user(), but record here.
|
Test user login (valid and invalid). Already done in login_user(), but record here.
|
||||||
@ -229,10 +254,11 @@ def test_login_user() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params))
|
test_endpoint("POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params))
|
||||||
|
|
||||||
|
|
||||||
def test_edit_profile() -> None:
|
def test_edit_profile() -> None:
|
||||||
"""
|
"""
|
||||||
Test editing user profile.
|
Test editing user profile.
|
||||||
@ -249,10 +275,11 @@ def test_edit_profile() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("POST", f"{BASE_URL}/users/me/edit-profile", data=patch_auth(params))
|
test_endpoint("POST", f"{BASE_URL}/users/me/edit-profile", data=patch_auth(params))
|
||||||
|
|
||||||
|
|
||||||
def test_forgot_password() -> None:
|
def test_forgot_password() -> None:
|
||||||
"""
|
"""
|
||||||
Test forgot password.
|
Test forgot password.
|
||||||
@ -265,10 +292,11 @@ def test_forgot_password() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("POST", f"{BASE_URL}/users/forgot-password", data=patch_auth(params))
|
test_endpoint("POST", f"{BASE_URL}/users/forgot-password", data=patch_auth(params))
|
||||||
|
|
||||||
|
|
||||||
def test_resend_confirm() -> None:
|
def test_resend_confirm() -> None:
|
||||||
"""
|
"""
|
||||||
Test resend confirmation email.
|
Test resend confirmation email.
|
||||||
@ -276,13 +304,11 @@ def test_resend_confirm() -> None:
|
|||||||
Payload: POST /users/me/resend-confirm with plat, guid, sid, seid, auth
|
Payload: POST /users/me/resend-confirm with plat, guid, sid, seid, auth
|
||||||
Expected: success=true
|
Expected: success=true
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"plat": PLAT, "guid": GUID, "sid": SID, "seid": SEID}
|
||||||
"plat": PLAT,
|
test_endpoint(
|
||||||
"guid": GUID,
|
"POST", f"{BASE_URL}/users/me/resend-confirm", data=patch_auth(params)
|
||||||
"sid": SID,
|
)
|
||||||
"seid": SEID
|
|
||||||
}
|
|
||||||
test_endpoint("POST", f"{BASE_URL}/users/me/resend-confirm", data=patch_auth(params))
|
|
||||||
|
|
||||||
def test_delete_account() -> None:
|
def test_delete_account() -> None:
|
||||||
"""
|
"""
|
||||||
@ -301,6 +327,7 @@ def test_delete_account() -> None:
|
|||||||
# test_endpoint("DELETE", f"{BASE_URL}/users/me", params=patch_auth(params))
|
# test_endpoint("DELETE", f"{BASE_URL}/users/me", params=patch_auth(params))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_mark_news_read() -> None:
|
def test_mark_news_read() -> None:
|
||||||
"""
|
"""
|
||||||
Test mark news as read.
|
Test mark news as read.
|
||||||
@ -313,9 +340,12 @@ def test_mark_news_read() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("POST", f"{BASE_URL}/users/me/mark-news-read", data=patch_auth(params))
|
test_endpoint(
|
||||||
|
"POST", f"{BASE_URL}/users/me/mark-news-read", data=patch_auth(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_rant() -> None:
|
def test_get_rant() -> None:
|
||||||
"""
|
"""
|
||||||
@ -330,9 +360,12 @@ def test_get_rant() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("GET", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params))
|
test_endpoint(
|
||||||
|
"GET", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_post_rant() -> None:
|
def test_post_rant() -> None:
|
||||||
"""
|
"""
|
||||||
@ -344,6 +377,7 @@ def test_post_rant() -> None:
|
|||||||
# Handled in setup
|
# Handled in setup
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_edit_rant() -> None:
|
def test_edit_rant() -> None:
|
||||||
"""
|
"""
|
||||||
Test edit rant.
|
Test edit rant.
|
||||||
@ -351,11 +385,11 @@ def test_edit_rant() -> None:
|
|||||||
Payload: POST /devrant/rants/{rant_id} with updated rant, tags, auth
|
Payload: POST /devrant/rants/{rant_id} with updated rant, tags, auth
|
||||||
Expected: success=true
|
Expected: success=true
|
||||||
"""
|
"""
|
||||||
data: Dict[str, Any] = {
|
data: Dict[str, Any] = {"rant": "Updated test rant", "tags": "test,python,update"}
|
||||||
"rant": "Updated test rant",
|
test_endpoint(
|
||||||
"tags": "test,python,update"
|
"POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", data=patch_auth(data)
|
||||||
}
|
)
|
||||||
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", data=patch_auth(data))
|
|
||||||
|
|
||||||
def test_delete_rant() -> None:
|
def test_delete_rant() -> None:
|
||||||
"""
|
"""
|
||||||
@ -364,13 +398,11 @@ def test_delete_rant() -> None:
|
|||||||
Payload: DELETE /devrant/rants/{rant_id} with plat, guid, sid, seid, auth
|
Payload: DELETE /devrant/rants/{rant_id} with plat, guid, sid, seid, auth
|
||||||
Expected: success=true
|
Expected: success=true
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"plat": PLAT, "guid": GUID, "sid": SID, "seid": SEID}
|
||||||
"plat": PLAT,
|
test_endpoint(
|
||||||
"guid": GUID,
|
"DELETE", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params)
|
||||||
"sid": SID,
|
)
|
||||||
"seid": SEID
|
|
||||||
}
|
|
||||||
test_endpoint("DELETE", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params))
|
|
||||||
|
|
||||||
def test_vote_rant() -> None:
|
def test_vote_rant() -> None:
|
||||||
"""
|
"""
|
||||||
@ -384,12 +416,17 @@ def test_vote_rant() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params))
|
test_endpoint(
|
||||||
|
"POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params)
|
||||||
|
)
|
||||||
params["vote"] = -1
|
params["vote"] = -1
|
||||||
params["reason"] = "1"
|
params["reason"] = "1"
|
||||||
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params))
|
test_endpoint(
|
||||||
|
"POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_favorite_rant() -> None:
|
def test_favorite_rant() -> None:
|
||||||
"""
|
"""
|
||||||
@ -398,14 +435,18 @@ def test_favorite_rant() -> None:
|
|||||||
Payload: POST /devrant/rants/{rant_id}/favorite or /unfavorite with plat, guid, sid, seid, auth
|
Payload: POST /devrant/rants/{rant_id}/favorite or /unfavorite with plat, guid, sid, seid, auth
|
||||||
Expected: success=true
|
Expected: success=true
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"plat": PLAT, "guid": GUID, "sid": SID, "seid": SEID}
|
||||||
"plat": PLAT,
|
test_endpoint(
|
||||||
"guid": GUID,
|
"POST",
|
||||||
"sid": SID,
|
f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/favorite",
|
||||||
"seid": SEID
|
data=patch_auth(params),
|
||||||
}
|
)
|
||||||
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/favorite", data=patch_auth(params))
|
test_endpoint(
|
||||||
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/unfavorite", data=patch_auth(params))
|
"POST",
|
||||||
|
f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/unfavorite",
|
||||||
|
data=patch_auth(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_rant_feed() -> None:
|
def test_get_rant_feed() -> None:
|
||||||
"""
|
"""
|
||||||
@ -414,14 +455,10 @@ def test_get_rant_feed() -> None:
|
|||||||
Payload: GET /devrant/rants with plat, guid, sid, seid, auth
|
Payload: GET /devrant/rants with plat, guid, sid, seid, auth
|
||||||
Expected: success=true, list of rants
|
Expected: success=true, list of rants
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"plat": PLAT, "guid": GUID, "sid": SID, "seid": SEID}
|
||||||
"plat": PLAT,
|
|
||||||
"guid": GUID,
|
|
||||||
"sid": SID,
|
|
||||||
"seid": SEID
|
|
||||||
}
|
|
||||||
test_endpoint("GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params))
|
test_endpoint("GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params))
|
||||||
|
|
||||||
|
|
||||||
def test_get_comment() -> None:
|
def test_get_comment() -> None:
|
||||||
"""
|
"""
|
||||||
Test get single comment.
|
Test get single comment.
|
||||||
@ -434,20 +471,24 @@ def test_get_comment() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("GET", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params))
|
test_endpoint(
|
||||||
|
"GET", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_post_comment() -> None:
|
def test_post_comment() -> None:
|
||||||
"""
|
"""
|
||||||
Test post comment. (Handled in post_test_comment for id)
|
Test post comment. (Handled in post_test_comment for id)
|
||||||
|
|
||||||
Payload: POST /devrant/rants/{rant_id}/comments with comment, auth
|
Payload: POST /devrant/rants/{rant_id}/comments with comment, auth
|
||||||
Expected: success=true, comment_id
|
Expected: success=true, comment_id
|
||||||
"""
|
"""
|
||||||
# Handled in setup
|
# Handled in setup
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_edit_comment() -> None:
|
def test_edit_comment() -> None:
|
||||||
"""
|
"""
|
||||||
Test edit comment.
|
Test edit comment.
|
||||||
@ -455,10 +496,11 @@ def test_edit_comment() -> None:
|
|||||||
Payload: POST /comments/{comment_id} with updated comment, auth
|
Payload: POST /comments/{comment_id} with updated comment, auth
|
||||||
Expected: success=true
|
Expected: success=true
|
||||||
"""
|
"""
|
||||||
data: Dict[str, Any] = {
|
data: Dict[str, Any] = {"comment": "Updated test comment"}
|
||||||
"comment": "Updated test comment"
|
test_endpoint(
|
||||||
}
|
"POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", data=patch_auth(data)
|
||||||
test_endpoint("POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", data=patch_auth(data))
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_comment() -> None:
|
def test_delete_comment() -> None:
|
||||||
"""
|
"""
|
||||||
@ -467,13 +509,11 @@ def test_delete_comment() -> None:
|
|||||||
Payload: DELETE /comments/{comment_id} with plat, guid, sid, seid, auth
|
Payload: DELETE /comments/{comment_id} with plat, guid, sid, seid, auth
|
||||||
Expected: success=true
|
Expected: success=true
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"plat": PLAT, "guid": GUID, "sid": SID, "seid": SEID}
|
||||||
"plat": PLAT,
|
test_endpoint(
|
||||||
"guid": GUID,
|
"DELETE", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params)
|
||||||
"sid": SID,
|
)
|
||||||
"seid": SEID
|
|
||||||
}
|
|
||||||
test_endpoint("DELETE", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params))
|
|
||||||
|
|
||||||
def test_vote_comment() -> None:
|
def test_vote_comment() -> None:
|
||||||
"""
|
"""
|
||||||
@ -487,9 +527,12 @@ def test_vote_comment() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}/vote", data=patch_auth(params))
|
test_endpoint(
|
||||||
|
"POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}/vote", data=patch_auth(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_notif_feed() -> None:
|
def test_get_notif_feed() -> None:
|
||||||
"""
|
"""
|
||||||
@ -504,10 +547,11 @@ def test_get_notif_feed() -> None:
|
|||||||
"plat": PLAT,
|
"plat": PLAT,
|
||||||
"guid": GUID,
|
"guid": GUID,
|
||||||
"sid": SID,
|
"sid": SID,
|
||||||
"seid": SEID
|
"seid": SEID,
|
||||||
}
|
}
|
||||||
test_endpoint("GET", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params))
|
test_endpoint("GET", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params))
|
||||||
|
|
||||||
|
|
||||||
def test_clear_notifications() -> None:
|
def test_clear_notifications() -> None:
|
||||||
"""
|
"""
|
||||||
Test clear notifications.
|
Test clear notifications.
|
||||||
@ -515,13 +559,11 @@ def test_clear_notifications() -> None:
|
|||||||
Payload: DELETE /users/me/notif-feed with plat, guid, sid, seid, auth
|
Payload: DELETE /users/me/notif-feed with plat, guid, sid, seid, auth
|
||||||
Expected: success=true
|
Expected: success=true
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"plat": PLAT, "guid": GUID, "sid": SID, "seid": SEID}
|
||||||
"plat": PLAT,
|
test_endpoint(
|
||||||
"guid": GUID,
|
"DELETE", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params)
|
||||||
"sid": SID,
|
)
|
||||||
"seid": SEID
|
|
||||||
}
|
|
||||||
test_endpoint("DELETE", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params))
|
|
||||||
|
|
||||||
def test_beta_list_signup() -> None:
|
def test_beta_list_signup() -> None:
|
||||||
"""
|
"""
|
||||||
@ -530,11 +572,10 @@ def test_beta_list_signup() -> None:
|
|||||||
Payload: GET https://www.hexicallabs.com/api/beta-list with email, platform, app
|
Payload: GET https://www.hexicallabs.com/api/beta-list with email, platform, app
|
||||||
Expected: Whatever the API returns (may not be JSON)
|
Expected: Whatever the API returns (may not be JSON)
|
||||||
"""
|
"""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {"email": TEST_EMAIL, "platform": "test_platform"}
|
||||||
"email": TEST_EMAIL,
|
test_endpoint(
|
||||||
"platform": "test_platform"
|
"GET", "https://www.hexicallabs.com/api/beta-list", params=patch_auth(params)
|
||||||
}
|
)
|
||||||
test_endpoint("GET", "https://www.hexicallabs.com/api/beta-list", params=patch_auth(params))
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@ -543,7 +584,7 @@ def main() -> None:
|
|||||||
global TEST_RANT_ID
|
global TEST_RANT_ID
|
||||||
TEST_RANT_ID = post_test_rant() or fetch_real_rant_id() or "1"
|
TEST_RANT_ID = post_test_rant() or fetch_real_rant_id() or "1"
|
||||||
global TEST_COMMENT_ID
|
global TEST_COMMENT_ID
|
||||||
TEST_COMMENT_ID = post_test_comment(TEST_RANT_ID) or "1"
|
TEST_COMMENT_ID = post_test_comment(TEST_RANT_ID) or "1"
|
||||||
|
|
||||||
test_register_user()
|
test_register_user()
|
||||||
test_login_user()
|
test_login_user()
|
||||||
@ -552,7 +593,7 @@ def main() -> None:
|
|||||||
test_resend_confirm()
|
test_resend_confirm()
|
||||||
test_mark_news_read()
|
test_mark_news_read()
|
||||||
test_get_rant()
|
test_get_rant()
|
||||||
test_post_rant()
|
test_post_rant()
|
||||||
test_edit_rant()
|
test_edit_rant()
|
||||||
test_vote_rant()
|
test_vote_rant()
|
||||||
test_favorite_rant()
|
test_favorite_rant()
|
||||||
@ -570,5 +611,6 @@ def main() -> None:
|
|||||||
test_delete_account()
|
test_delete_account()
|
||||||
save_results()
|
save_results()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
Loading…
Reference in New Issue
Block a user