converted to config.py

This commit is contained in:
2026-02-23 23:31:15 +00:00
parent 639ab4dd16
commit e5d673a8dd
6 changed files with 131 additions and 61 deletions

View File

@@ -9,7 +9,6 @@ import traceback
import datetime as dt import datetime as dt
from pathlib import Path from pathlib import Path
from logger import Logger from logger import Logger
from jsonwrapper import AutoSaveDict
from typing import ( from typing import (
Optional, Optional,
Callable, Callable,
@@ -24,53 +23,38 @@ from bot import (
Row, Row,
) )
# configuration is now a Python module instead of a JSON file
from config import config as cfg, TEMPLATE
def config_app(path: Path) -> AutoSaveDict:
config = {
'client_id': '',
'client_secret': '',
'user_agent': '',
'username': '',
'password': '',
'sub_name': '',
'max_days': '',
'max_posts': '',
'sleep_minutes': '',
}
configuration: List[List[str]] = [] # the old JSON-based interactive configuration helper has been removed. the
# values are now stored in ``config/config.py``. ``cfg`` above is a simple
# dict containing the settings; callers should treat numeric entries as
# integers.
if not os.path.exists(path):
for key, _ in config.items():
config_name = ' '.join(key.split('_')).title()
user_inp = input(f"{config_name}: ")
configuration.append([key, user_inp])
for config_name, value in configuration:
config[config_name] = value
config_handler = AutoSaveDict(
path,
**config
)
return config_handler
config_dir = Path(utils.BASE_DIR, 'config') config_dir = Path(utils.BASE_DIR, 'config')
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
config_file = Path(config_dir, 'config.json') config_path = Path(config_dir, 'config.py')
handler = config_app(config_file) # ensure a configuration file exists - if not, write the template and exit so
handler.init() # the user can edit it.
if not config_path.exists():
config_path.write_text(TEMPLATE)
print(f"Created new configuration template at {config_path!r}.\n"
"Please populate the values and restart the bot.")
sys.exit(0)
posts = Posts('deleted_posts', config_dir) posts = Posts('deleted_posts', config_dir)
logger = Logger(1) logger = Logger(1)
untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED) untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED)
posts.init() posts.init()
reddit = praw.Reddit( reddit = praw.Reddit(
client_id=handler['client_id'], client_id=cfg['client_id'],
client_secret=handler['client_secret'], client_secret=cfg['client_secret'],
user_agent=handler['user_agent'], user_agent=cfg['user_agent'],
username=handler['username'], username=cfg['username'],
password=handler['password'], password=cfg['password'],
) )
@@ -123,7 +107,7 @@ def notify_if_error(func: Callable[..., int]) -> Callable[..., int]:
msg = f"Error with '{bot_name}':\n\n{full_error}\n\nPlease report to author ({author})" msg = f"Error with '{bot_name}':\n\n{full_error}\n\nPlease report to author ({author})"
send_modmail( send_modmail(
reddit, reddit,
handler['sub_name'], cfg['sub_name'],
f'An error has occured with {utils.BOT_NAME} msg', f'An error has occured with {utils.BOT_NAME} msg',
msg msg
) )
@@ -167,13 +151,13 @@ def main() -> int:
posts_to_delete: Set[Row] = set() posts_to_delete: Set[Row] = set()
ignore_methods = ['Removed by mod',] ignore_methods = ['Removed by mod',]
if utils.parse_cmd_line_args(sys.argv, logger, config_file, posts): if utils.parse_cmd_line_args(sys.argv, logger, config_path, posts):
return 0 return 0
saved_submission_ids = {row.post_id for row in posts.fetch_all()} saved_submission_ids = {row.post_id for row in posts.fetch_all()}
max_posts = handler['max_posts'] max_posts = cfg.get('max_posts')
limit = int(max_posts) if max_posts else None limit = int(max_posts) if max_posts else None
sub_name = handler['sub_name'] sub_name = cfg['sub_name']
for submission in reddit.subreddit(sub_name).new(limit=limit): for submission in reddit.subreddit(sub_name).new(limit=limit):
try: try:
@@ -185,7 +169,7 @@ def main() -> int:
for stored_post in posts.fetch_all(): for stored_post in posts.fetch_all():
try: try:
submission = reddit.submission(id=stored_post.post_id) submission = reddit.submission(id=stored_post.post_id)
max_days = int(handler['max_days']) max_days = int(cfg['max_days'])
created = utils.string_to_dt(stored_post.record_created).date() created = utils.string_to_dt(stored_post.record_created).date()
flair = utils.get_flair(submission.link_flair_text) flair = utils.get_flair(submission.link_flair_text)
@@ -199,7 +183,7 @@ def main() -> int:
if method not in ignore_methods: if method not in ignore_methods:
send_modmail( send_modmail(
reddit, reddit,
handler['sub_name'], cfg['sub_name'],
"User's account has been deleted", "User's account has been deleted",
utils.modmail_removal_notification(stored_post, 'Account has been deleted') utils.modmail_removal_notification(stored_post, 'Account has been deleted')
) )
@@ -213,7 +197,7 @@ def main() -> int:
msg = utils.modmail_removal_notification(stored_post, method) msg = utils.modmail_removal_notification(stored_post, method)
send_modmail( send_modmail(
reddit, reddit,
handler['sub_name'], cfg['sub_name'],
'A post has been deleted', 'A post has been deleted',
msg msg
) )
@@ -237,7 +221,7 @@ def main() -> int:
logger.info(f"Total posts deleted: {len(posts_to_delete)}") logger.info(f"Total posts deleted: {len(posts_to_delete)}")
# wait before the next cycle # wait before the next cycle
sleep_minutes = int(handler['sleep_minutes']) if handler['sleep_minutes'] else 5 sleep_minutes = int(cfg.get('sleep_minutes', 5))
time.sleep(sleep_minutes * 60) time.sleep(sleep_minutes * 60)
# end of while True # end of while True

View File

@@ -55,27 +55,37 @@ Ban Template;
def parse_cmd_line_args(args: List[str], logger: Logger, config_file: Path, posts: Posts) -> bool: def parse_cmd_line_args(args: List[str], logger: Logger, config_file: Path, posts: Posts) -> bool:
"""Parse a very small set of operations from ``sys.argv``.
``config_file`` now refers to the Python configuration module path
(typically ``.../config/config.py``). ``reset_config`` will overwrite the
file with the default template, which is imported from the configuration
module itself so that it does not need to be duplicated here.
"""
help_msg = """Command line help prompt help_msg = """Command line help prompt
Command: help Command: help
Args: [] Args: []
Decription: Prints the help prompt Description: Prints the help prompt
Command: reset_config Command: reset_config
Args: [] Args: []
Decription: Reset the bot credentials Description: Overwrite the Python configuration file with default values
Command: reset_db Command: reset_db
Args: [] Args: []
Decription: Reset the database Description: Reset the database
""" """
if len(args) > 1: if len(args) > 1:
if args[1] == 'help': if args[1] == 'help':
logger.info(help_msg) logger.info(help_msg)
elif args[1] == 'reset_config': elif args[1] == 'reset_config':
# write the template text back to the configuration file. import
# TEMPLATE lazily in case the module has not yet been created.
try: try:
os.remove(config_file) from config import TEMPLATE
except FileNotFoundError: config_file.write_text(TEMPLATE)
logger.error("No configuration file found") except Exception:
logger.error("Unable to reset configuration file")
elif args[1] == 'reset_db': elif args[1] == 'reset_db':
try: try:
os.remove(posts.path) os.remove(posts.path)

View File

@@ -1,11 +1,19 @@
import unittest import unittest
import datetime as dt import datetime as dt
from pathlib import Path
from .actions import ( from .actions import (
Flair, Flair,
get_flair, get_flair,
string_to_dt, string_to_dt,
submission_is_older, submission_is_older,
parse_cmd_line_args,
) )
from logger import Logger
class DummyPosts:
def __init__(self, path):
self.path = path
class TestActions(unittest.TestCase): class TestActions(unittest.TestCase):
@@ -46,3 +54,27 @@ class TestActions(unittest.TestCase):
post_made = today - dt.timedelta(days=(max_days + 1)) post_made = today - dt.timedelta(days=(max_days + 1))
result = submission_is_older(post_made.date(), max_days) result = submission_is_older(post_made.date(), max_days)
self.assertTrue(result) self.assertTrue(result)
def test_parse_cmd_line_args_reset_config_and_db(self) -> None:
tmp = Path(__file__).parent / "tmp_test"
tmp.mkdir(exist_ok=True)
cfg_file = tmp / "config.py"
# ensure file exists with junk content
cfg_file.write_text("not important")
db_file = tmp / "db.sqlite"
db_file.write_text("x")
posts = DummyPosts(db_file)
logger = Logger(1)
# reset_config should rewrite the config file
result = parse_cmd_line_args(["prog", "reset_config"], logger, cfg_file, posts)
self.assertTrue(result)
self.assertTrue(cfg_file.exists())
content = cfg_file.read_text()
self.assertIn("client_id", content)
# reset_db should remove the database file
db_file.write_text("x")
result = parse_cmd_line_args(["prog", "reset_db"], logger, cfg_file, posts)
self.assertTrue(result)
self.assertFalse(db_file.exists())

View File

@@ -0,0 +1,17 @@
# DeletedPosts Bot
Configuration used to live in a JSON file, but it has been migrated to a
Python module under ``config/config.py``.
When you first run the application a template file will be created for you in
the ``config`` directory. Edit the values in the ``config`` dictionary and
restart the bot.
You can also reset the configuration back to the default template by invoking
``reset_config`` on the command line:
```
python -m Bot.main reset_config
```
Other command line actions (``help`` and ``reset_db``) remain unchanged.

View File

@@ -1,11 +0,0 @@
{
"client_id": "",
"client_secret": "",
"user_agent": "",
"username": "",
"password": "",
"sub_name": "",
"max_days": "180",
"max_posts": "180",
"sleep_minutes": "5"
}

38
config/config.py Normal file
View File

@@ -0,0 +1,38 @@
"""
Configuration for the DeletedPosts bot.
This module exposes a single dictionary called ``config`` which holds all
of the parameters required to connect to Reddit and control the behaviour of
the bot. When you first run the program the file will be created for you
with empty values; you should edit it before starting the bot. You can also
reset the file to defaults by running the application with the
``reset_config`` command-line argument.
Example usage::
from config import config
print(config['client_id'])
"""
config = {
"client_id": "",
"client_secret": "",
"user_agent": "",
"username": "",
"password": "",
"sub_name": "",
# numeric settings are stored as integers here rather than strings
"max_days": 180,
"max_posts": 180,
"sleep_minutes": 5,
}
# same data as a text template. both ``main.py`` and ``utils.actions`` import
# this so that the file can be created or reset without duplicating the
# literal configuration body.
TEMPLATE = f"""# configuration for DeletedPosts bot
# edit the values of the dictionary below and restart the bot
config = {config!r}
"""