MIL-001 foundation #32

Merged
Tirsvad merged 2 commits from mil-001-foundation into main 2026-10-28 07:37:41 +01:00
11 changed files with 3374 additions and 8 deletions
+6
View File
@@ -0,0 +1,6 @@
# Copy to .env and fill in. .env is ignored by git; never commit it.
SMTP_ADDRESS="smtp.gmail.com"
EMAIL_ADDRESS="you@example.com"
EMAIL_PASSWORD="Use_an_App_Password_from_your_email_provider"
# Optional: track another product instead of the default URL
# PRODUCT_URL="https://www.amazon.com/dp/B075CYMYK6"
+29
View File
@@ -0,0 +1,29 @@
# Secrets (never commit)
.env
.env.*
!.env.example
# Python
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
# Virtual environments
.venv/
venv/
# Doxygen output
docs/doxygen/
# Editors and OS
.idea/
.vscode/
.DS_Store
Thumbs.db
+2967
View File
File diff suppressed because it is too large Load Diff
+155 -1
View File
@@ -1,2 +1,156 @@
# 047-amazon-price-tracker # 🚀 Amazon Price Tracker
A Python bot for Day 47 of Udemy's "100 Days of Code: The Complete Python Pro Bootcamp": it reads a product page, extracts the price and sends an e-mail alert when the price drops below a target. Made for course participants who want to compare solutions and for visitors who browse for ideas.
## 📚 Table of Contents
- [Overview](#-overview)
- [Requirements](#-requirements)
- [Setup](#-setup)
- [Run](#-run)
- [Tests](#-tests)
- [License](#-license)
- [Links](#-links)
- [Appendix: Project documents](#-appendix-project-documents)
## 🧭 Overview
The assignment is built in four lecture steps. Each step was delivered on its own branch and pull request, and each has a screenshot and a wiki page in English, Danish and Bahasa Malaysia that explains what it teaches.
| Step | What it adds | Status |
| --- | --- | --- |
| 1 | Scrape the price from the practice page with BeautifulSoup | planned (MIL-002) |
| 2 | E-mail alert below the target price, secrets in `.env` | planned (MIL-003) |
| 3 | Browser-like request headers | planned (MIL-004) |
| 4 | The live Amazon page | planned (MIL-005) |
Repository layout:
| Path | What it is for |
| --- | --- |
| `src/amazon_price_tracker/` | The package; `constants.py` is the only place for constants |
| `tests/` | `pytest` tests; they never use the network or a real mail server |
| `docs/` | Project documents (Business Case, plan, milestones, reviews) and the wiki pages |
| `Doxyfile` | Doxygen configuration for the source comments |
| `.env.example` | The settings you put in your own `.env` |
## 📋 Requirements
- Python 3.13 or newer.
- Runtime packages (installed by `pip`): `requests`, `beautifulsoup4`, `python-dotenv`. Nothing else is needed to run the bot.
- Optional, for development: `pytest`, `ruff`, `mypy` (the `dev` extra) and [Doxygen](https://www.doxygen.nl/) for the API documentation.
## 🛠️ Setup
Create a local virtual environment named `.venv`, activate it and upgrade `pip`:
```bash
python -m venv .venv
```
Activate it (Linux and macOS):
```bash
source .venv/bin/activate
```
Activate it (Windows PowerShell):
```powershell
.venv\Scripts\Activate.ps1
```
Activate it (Windows Git Bash):
```bash
source .venv/Scripts/activate
```
Upgrade `pip` and install the project with the development tools:
```bash
python -m pip install --upgrade pip
```
```bash
python -m pip install -e ".[dev]"
```
For use without development tools, install with `python -m pip install -e .`.
Create your own settings file from the example and fill in the values. `.env` is ignored by git; never commit it:
```bash
cp .env.example .env
```
Gmail needs 2-Step Verification and an App Password; use the App Password as `EMAIL_PASSWORD`. SMTP addresses: Gmail `smtp.gmail.com`, Hotmail `smtp.live.com`, Outlook `outlook.office365.com`, Yahoo `smtp.mail.yahoo.com`. The bot connects on port 587.
## ▶️ Run
The runnable steps are added milestone by milestone (see the table above). After step 1 the bot starts with:
```bash
python -m amazon_price_tracker
```
## 🧪 Tests
```bash
python -m pytest
```
Lint and type checks:
```bash
ruff check .
```
```bash
mypy
```
Build the API documentation (written to `docs/doxygen/`, which is git-ignored):
```bash
doxygen Doxyfile
```
## 📄 License
GNU Affero General Public License v3.0, see [LICENSE](LICENSE).
## 🔗 Links
- [Repository][repository]
- [Documentation][documentation]
- [Issue tracker][issues]
## 📎 Appendix: Project documents
The documents a newcomer needs, in reading order.
| Document | What it tells you |
| --- | --- |
| [AGENTS.md][agents] | The rules AI agents follow in this repository |
| [Business Case][BC-001] | Why the project exists, its scope and success criteria |
| [Stakeholder Analysis][SA-001] | Who is involved; the stakeholder IDs used for owners and reviewers |
| [Project Plan][PP-001] | The gateways and their schedule |
| [Milestones][milestones] | One document per gateway: Go/No-Go criteria and tasks |
| [Artifact registry][registry] | Where each artifact type lives, the next version, the PO language and domain |
| [Framework README][framework-readme] | The framework this project uses |
| [Framework changelog][framework-changelog] | What changed in each framework release |
---
[repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker
[documentation]: ./docs/
[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/issues
[agents]: ./AGENTS.md
[BC-001]: ./docs/business-case.md
[SA-001]: ./docs/stakeholder-analysis.md
[PP-001]: ./docs/project-plan.md
[milestones]: ./docs/milestones/
[registry]: ./docs/artifact-registry.md
[framework-readme]: ./framework/README.md
[framework-changelog]: ./framework/CHANGELOG.md
+1 -1
View File
@@ -15,7 +15,7 @@ document of a type. `Primary File` may contain a glob (e.g.
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 007 | | MIL | Milestone / Gateway | docs/milestones/*.md | 007 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 010 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 011 |
## Languages ## Languages
+6 -6
View File
@@ -31,12 +31,12 @@ Schedule the six gateways (Go/No-Go decision points) that deliver the Day 47 Ama
| Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone | | Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone |
| --- | --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- | --- |
| MIL-001 | `docs/milestones/mil-001-foundation.md` | 2026-10-28 | 2026-10-28 | S01 | none (technical tasks) | Branch `mil-001-foundation` merged: `pyproject.toml`, `.gitignore`, `.env.example`, `constants.py`, `Doxyfile`, README from the framework template, a passing smoke test, and the repository description and topics on the host. | not yet synced | | MIL-001 | `docs/milestones/mil-001-foundation.md` | 2026-10-28 | 2026-10-28 | S01 | none (technical tasks) | Branch `mil-001-foundation` merged: `pyproject.toml`, `.gitignore`, `.env.example`, `constants.py`, `Doxyfile`, README from the framework template, a passing smoke test, and the repository description and topics on the host. | [Milestone 271](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/milestones/271) |
| MIL-002 | `docs/milestones/mil-002-scrape-price.md` | 2026-10-29 | 2026-10-29 | S01 | none (technical tasks) | Branch `mil-002-scrape-price` merged: a scraper that returns the title and the price as a `float`, a saved copy of the practice page used by tests, a screenshot of the step, and the README section for it. | not yet synced | | MIL-002 | `docs/milestones/mil-002-scrape-price.md` | 2026-10-29 | 2026-10-29 | S01 | none (technical tasks) | Branch `mil-002-scrape-price` merged: a scraper that returns the title and the price as a `float`, a saved copy of the practice page used by tests, a screenshot of the step, and the README section for it. | [Milestone 272](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/milestones/272) |
| MIL-003 | `docs/milestones/mil-003-email-alert.md` | 2026-10-30 | 2026-10-30 | S01 | none (technical tasks) | Branch `mil-003-email-alert` merged: an alert that builds and sends an e-mail with title, price and buy link when the price is below target, with credentials from `.env`, tests with a mocked SMTP server, a screenshot, and the README section. | not yet synced | | MIL-003 | `docs/milestones/mil-003-email-alert.md` | 2026-10-30 | 2026-10-30 | S01 | none (technical tasks) | Branch `mil-003-email-alert` merged: an alert that builds and sends an e-mail with title, price and buy link when the price is below target, with credentials from `.env`, tests with a mocked SMTP server, a screenshot, and the README section. | [Milestone 273](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/milestones/273) |
| MIL-004 | `docs/milestones/mil-004-request-headers.md` | 2026-10-31 | 2026-10-31 | S01 | none (technical tasks) | Branch `mil-004-request-headers` merged: requests carry the headers from `constants.py`, tests prove it, a screenshot and a README section. | not yet synced | | MIL-004 | `docs/milestones/mil-004-request-headers.md` | 2026-10-31 | 2026-10-31 | S01 | none (technical tasks) | Branch `mil-004-request-headers` merged: requests carry the headers from `constants.py`, tests prove it, a screenshot and a README section. | [Milestone 274](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/milestones/274) |
| MIL-005 | `docs/milestones/mil-005-live-amazon.md` | 2026-11-01 | 2026-11-01 | S01 | none (technical tasks) | Branch `mil-005-live-amazon` merged: URL and selectors configurable for the live page, a detection of a CAPTCHA page with a clear message, tests, a screenshot of the result, and the README section including the limits. | not yet synced | | MIL-005 | `docs/milestones/mil-005-live-amazon.md` | 2026-11-01 | 2026-11-01 | S01 | none (technical tasks) | Branch `mil-005-live-amazon` merged: URL and selectors configurable for the live page, a detection of a CAPTCHA page with a clear message, tests, a screenshot of the result, and the README section including the limits. | [Milestone 275](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/milestones/275) |
| MIL-006 | `docs/milestones/mil-006-wiki-screenshots.md` | 2026-11-02 to 2026-11-04 | 2026-11-04 | S01 | none (technical tasks) | Wiki pages in `en`, `da` and `ms`, one per language per lecture step plus a home page, each explaining what was gained and showing the step screenshot; the wiki is published on the host. | not yet synced | | MIL-006 | `docs/milestones/mil-006-wiki-screenshots.md` | 2026-11-02 to 2026-11-04 | 2026-11-04 | S01 | none (technical tasks) | Wiki pages in `en`, `da` and `ms`, one per language per lecture step plus a home page, each explaining what was gained and showing the step screenshot; the wiki is published on the host. | [Milestone 276](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/milestones/276) |
## Timeline diagram ## Timeline diagram
+62
View File
@@ -0,0 +1,62 @@
# RC-010 Code review of MIL-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-010 |
| CrossReference | [MIL-001], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [88d5fca] |
---
## Artifact Under Review
- Instance reviewed: [MIL-001]
- Checklist used: [QC-PY-001]
- Scope: full review
- Language and domain: n/a (technical)
- Language reviewer: none
- Mode: autonomous, not independent (AI as stand-in for S01); the author and the stand-in are both the AI working for S01
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules and functions are snake_case, classes PascalCase, constants UPPER_SNAKE |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (price, target, headers); no abbreviations left unexplained |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | ruff format and ruff check pass: 5 pytest tests, ruff 0 findings, mypy strict 0 errors |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | All signatures annotated; mypy --strict passes: 5 pytest tests, ruff 0 findings, mypy strict 0 errors |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No bare except; errors are specific and chained with `raise ... from` where a cause exists |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no shadowed builtins |
| 7 | Files, locks and connections are managed with context managers | Pass | No open files or connections without a context manager |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules and functions have docstrings with Doxygen tags |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | Output uses print only for the assignment's visible result in main; no secret is printed |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No Design Class Diagram exists; the milestone's technical tasks need none (Needs use case: No) |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | Tests exist, are named for behaviour and use no network or real mail server: 5 pytest tests, ruff 0 findings, mypy strict 0 errors |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | mypy --strict passes; no Any |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | Dependencies are in pyproject.toml with lower bounds; none unused |
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | Constants live only in constants.py |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | Imports point inward; no cycles |
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | Small modules with one reason to change; narrow protocols where used |
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | Only the package root exists: constants.py is shared by all layers; the layer packages arrive with the features of MIL-002 onwards. Tests are under tests/unit/ |
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File names are English snake_case |
## Overall Verdict
Go — all Mandatory criteria pass or are not applicable.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None: no Fail and no Pending criterion | S01 | 2026-10-28 |
---
[MIL-001]: ../../milestones/mil-001-foundation.md
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[88d5fca]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker/commit/88d5fcaddb5ea71bc5a493c20ab046a645b308b5
+61
View File
@@ -0,0 +1,61 @@
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "amazon-price-tracker"
version = "0.1.0"
description = "Day 47 of 100 Days of Code: scrape a product price and send an e-mail alert when it drops below a target."
readme = "README.md"
requires-python = ">=3.13"
license = "AGPL-3.0-or-later"
license-files = ["LICENSE"]
authors = [{ name = "Jens Tirsvad Nielsen" }]
keywords = ["web-scraping", "beautifulsoup", "smtp", "price-tracker"]
classifiers = [
"Programming Language :: Python :: 3.13",
"Environment :: Console",
"Topic :: Education",
]
dependencies = [
"requests>=2.32",
"beautifulsoup4>=4.12",
"python-dotenv>=1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"ruff>=0.6",
"mypy>=1.11",
"types-requests>=2.32",
]
[project.urls]
Repository = "https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/047-amazon-price-tracker"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-ra"
[tool.ruff]
line-length = 88
target-version = "py313"
src = ["src", "tests"]
extend-exclude = ["framework"]
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "D"]
ignore = ["D203", "D213"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["D"]
[tool.mypy]
strict = true
python_version = "3.13"
files = ["src", "tests"]
+7
View File
@@ -0,0 +1,7 @@
"""Amazon price tracker package.
@file __init__.py
@brief Package marker for the Amazon price tracker.
"""
__all__: list[str] = []
+39
View File
@@ -0,0 +1,39 @@
"""Constants of the Amazon price tracker.
@file constants.py
@brief The only place that defines constants (URLs, limits, environment names).
"""
from typing import Final
## @brief Practice page used instead of Amazon while developing.
PRACTICE_URL: Final[str] = "https://appbrewery.github.io/instant_pot/"
## @brief Amazon product page to track.
AMAZON_URL: Final[str] = (
"https://www.amazon.com/dp/B075CYMYK6"
"?psc=1&ref_=cm_sw_r_cp_ud_ct_FM9M699VKHTT47YD50Q6"
)
## @brief Price below which an alert is sent.
# The assignment asks for a floating point price, so float is used here
# although money is normally Decimal.
TARGET_PRICE: Final[float] = 100.0
## @brief Seconds to wait for an HTTP response.
REQUEST_TIMEOUT_SECONDS: Final[int] = 10
## @brief SMTP submission port (STARTTLS).
SMTP_PORT: Final[int] = 587
## @brief Environment variable holding the SMTP server address.
ENV_SMTP_ADDRESS: Final[str] = "SMTP_ADDRESS"
## @brief Environment variable holding the sender e-mail address.
ENV_EMAIL_ADDRESS: Final[str] = "EMAIL_ADDRESS"
## @brief Environment variable holding the e-mail password.
ENV_EMAIL_PASSWORD: Final[str] = "EMAIL_PASSWORD"
## @brief Environment variable that optionally overrides the product URL.
ENV_PRODUCT_URL: Final[str] = "PRODUCT_URL"
+41
View File
@@ -0,0 +1,41 @@
"""Smoke tests for the constants module.
@file test_constants.py
@brief Checks the shape of the constants.
"""
from amazon_price_tracker import constants
ENV_NAMES: list[str] = [
constants.ENV_SMTP_ADDRESS,
constants.ENV_EMAIL_ADDRESS,
constants.ENV_EMAIL_PASSWORD,
constants.ENV_PRODUCT_URL,
]
def test_urls_use_https() -> None:
"""Both product URLs are https."""
assert constants.PRACTICE_URL.startswith("https://")
assert constants.AMAZON_URL.startswith("https://")
def test_target_price_is_positive_float() -> None:
"""The target price is a positive float."""
assert isinstance(constants.TARGET_PRICE, float)
assert constants.TARGET_PRICE > 0
def test_env_names_are_non_empty() -> None:
"""Every environment variable name has text."""
assert all(name.strip() for name in ENV_NAMES)
def test_env_names_are_unique() -> None:
"""No two environment variable names collide."""
assert len(set(ENV_NAMES)) == len(ENV_NAMES)
def test_smtp_port_is_starttls_port() -> None:
"""The SMTP port is 587."""
assert constants.SMTP_PORT == 587