Thanks for your interest in contributing! This document outlines the conventions and workflow to follow.
# 1. Clone & enter the project
git clone https://github.com/Aliferous-spec/price-monitor.git
cd price-monitor
# 2. Create a virtual environment (Python 3.9+)
python -m venv .venv
source .venv/bin/activate # Linux / macOS
.venv\Scripts\activate # Windows
# 3. Install dependencies
pip install -r requirements.txt
# 4. Create a local config (not tracked by git)
cp config.example.py config.py
# 5. Verify everything works
python demo.py
config.pyis git-ignored — never commit secrets. Useconfig.example.pyas a template for new config keys.
price-monitor/
├── main.py # Entry point — config loading, main loop, alert logic
├── scraper.py # HTTP fetch + CSS-selector price extraction
├── storage.py # JSON-lines persistence, history queries, drop detection
├── notifier.py # Pluggable notification channels (email, Telegram, …)
├── visualization.py # matplotlib chart rendering
├── demo.py # One-shot smoke test of scraper / storage / notifier
├── config.example.py # Config template (copy → config.py)
└── requirements.txt # Third-party dependencies
The three core modules (scraper, storage, notifier) are independent — each can be tested in isolation via demo.py.
- Python 3.9+ with
from __future__ import annotationsfor modern type hints. - Use type annotations on all public functions.
- Follow PEP 8 — 4 spaces, 100-char lines.
- Docstrings use Google style (triple-quote,
Args:/Returns:sections). - Keep dependencies minimal —
requests,beautifulsoup4,python-dotenv,matplotlib,numpy. Justify any new dependency. - Config keys are
UPPER_SNAKE_CASE; all are read fromconfig.pywith env-var overrides.
notifier.py uses a register decorator — adding a new channel is two steps:
# In notifier.py (or a new file you import from notifier.py)
from notifier import register
@register("slack")
def send_slack(config: dict, subject: str, body: str) -> bool:
"""Send a price alert to a Slack webhook."""
webhook_url = config.get("SLACK_WEBHOOK_URL")
if not webhook_url:
return False
# … your webhook logic …
return True- The function name doesn't matter — the
@register("name")string is the channel key. - Return
Trueon success,Falseon failure. - If the required config keys are missing, return
Falsesilently (the caller handles it). - Add any new config keys to
config.example.pywith comments.
The default scraper uses requests + BeautifulSoup + a CSS selector. If you want to add a different backend (Playwright, Selenium, an API client):
- Add a new module (e.g.
scraper_playwright.py) with a function matching theget_current_price(url, selector, **kwargs) -> float | Nonesignature. - Wire it into
main.pybehind a config flag (SCRAPER_BACKEND = "playwright"). - Update
demo.pyto exercise the new backend.
- Run
demo.py— all three checks must pass. - Test any new notifier with a real config (even just once).
- Update
config.example.pyif you added config keys. - Update
README.mdif the change affects user-facing behaviour. - Keep commits focused — one logical change per commit, with a clear message.
Write commits in the present tense, starting with a lowercase verb:
add Slack notifier channel
fix price regex for European formats
update README with Telegram setup steps
Please include:
- What you did — the command or code that triggered the problem.
- What you expected — the behaviour you wanted.
- What happened — the actual output, error message, or unexpected result.
- Environment — OS, Python version (
python --version), and the target website (if relevant).
By contributing, you agree that your code will be licensed under the same MIT License as the project.