Author SHA1 Message Date
Tirsvad e73218d9f4 feat: implement CLI program with user prompts and tests
- Add main.py with input prompts, bid validation, screen clearing,
  winner announcement
- Add tests/test_main.py with monkeypatched input tests
- Update docs/plan.md to mark Milestone 3 as complete
2026-10-04 14:13:01 +08:00
Tirsvad cebed68105 Merge pull request 'Milestone 2: Core auction logic' (#14) from milestone/02-core-logic into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 4s
Reviewed-on: #14
2026-10-04 08:09:33 +02:00
Tirsvad 4bc7012e77 refactor: remove env.py module and update plan
- Delete env.py module (functionality moved or no longer needed)
- Update docs/plan.md to reflect Milestone 2 completion
- Clean up constants.py by removing unused environment-related constants
2026-10-04 14:07:29 +08:00
Tirsvad d2cf059337 feat: implement core auction logic and env loader
- Add auction.py with add_bid and find_winner functions
- Add env.py with stdlib-based .env loader (no python-dotenv)
- Add tests/test_auction.py with test cases for auction functions
- Remove .env.example (tokens not meant to be committed)
- Update docs/plan.md to mark Milestone 2 as complete
2026-10-04 14:03:22 +08:00
Tirsvad bcea547c8b Merge pull request 'Milestone 1: Project scaffold' (#13) from milestone/01-scaffold into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Reviewed-on: #13
2026-10-04 07:54:59 +02:00
Tirsvad ffdf64400f feat: scaffold project structure and add configuration files
- Add src/secret_auction package with __init__.py and constants.py
- Configure pyproject.toml with setuptools, pytest, and console script
- Add Doxyfile and .env.example for documentation and environment setup
- Update .gitignore to exclude Doxygen output directory
- Include tests directory placeholder and docs/plan.md for milestone
  tracking
2026-10-04 13:53:07 +08:00
11 changed files with 274 additions and 0 deletions
+3
View File
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.pypirc
# Doxygen output
docs/doxygen/
+12
View File
@@ -0,0 +1,12 @@
PROJECT_NAME = "Secret Auction"
PROJECT_BRIEF = "A blind auction command line program"
OUTPUT_DIRECTORY = docs/doxygen
INPUT = src README.md
FILE_PATTERNS = *.py *.md
RECURSIVE = YES
OPTIMIZE_OUTPUT_JAVA = YES
EXTRACT_ALL = YES
USE_MDFILE_AS_MAINPAGE = README.md
GENERATE_HTML = YES
GENERATE_LATEX = NO
QUIET = YES
+40
View File
@@ -0,0 +1,40 @@
# Plan: Secret Auction Program (Day 9)
## Context
Udemy 100 Days of Code blind-auction CLI. The repository uses a Python `.gitignore`, an AGPL `LICENSE` and a git-ignored `.env` (tokens, never printed or committed). Python 3.13 or newer is required.
## Working method
Work on ONE milestone at a time. Each milestone is one branch (`milestone/NN-name`) and one pull request. When a milestone is finished, stop, verify it and review before starting the next. Milestones and issues are also tracked on the Gitea repository (via API, token from `.env`).
## Milestone 1 - Project scaffold and repo metadata (`milestone/01-scaffold`)
- [x] Create `src/secret_auction/__init__.py`, `tests/`, `docs/`
- [x] `pyproject.toml`: `requires-python >=3.13`, src layout, no runtime deps, `dev` extra (`pytest`), pytest config, console script
- [x] `.env.example`; `.env` and `.venv/` ignored; add `docs/doxygen/` to `.gitignore`
- [x] `constants.py`
- [x] `Doxyfile`
- [x] Set repository description and topics (`python`, `udemy`, `100-days-of-code`, `blind-auction`, `beginner`) via API
- [x] Create milestones and issues on the host (milestones M1-M4, issues #1-#12)
- [x] Verify: create `.venv`, upgrade pip, `pip install -e ".[dev]"`
- [x] Save plan and milestones in `docs/plan.md`
## Milestone 2 - Core auction logic (`milestone/02-core-logic`)
- [x] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty)
- [x] `tests/test_auction.py`
- Verify: `pytest` passes
## Milestone 3 - CLI program (`milestone/03-cli`)
- [x] `main.py`: prompt name and numeric bid (re-prompt on invalid), bids dict kept outside the loop, "other bidders? yes/no", clear screen with newlines between bidders, announce the winner
- [x] `tests/test_main.py` with monkeypatched `input`
- Verify: `pytest`; scripted run `python -m secret_auction.main` with two bidders
## Milestone 4 - Documentation (`milestone/04-docs`)
- [ ] `README.md` from the template (Overview, Requirements, Setup incl. creating/activating `.venv` and `python -m pip install --upgrade pip`, Run, Tests, License AGPL, Links)
- [ ] `docs/flowchart.md` (Mermaid plus link to the diagrams.net chart)
- [ ] Generate Doxygen docs if `doxygen` is installed
- Verify: README commands work verbatim; `.env` stays untracked
+26
View File
@@ -0,0 +1,26 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "secret-auction"
version = "0.1.0"
description = "A beginner-friendly blind auction program (Udemy 100 Days of Code, Day 9)."
readme = "README.md"
requires-python = ">=3.13"
license = "AGPL-3.0-or-later"
license-files = ["LICENSE"]
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=8"]
[project.scripts]
secret-auction = "secret_auction.main:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
+5
View File
@@ -0,0 +1,5 @@
"""! @file __init__.py
@brief Secret Auction package: a blind auction command line program.
"""
__version__ = "0.1.0"
+27
View File
@@ -0,0 +1,27 @@
"""! @file auction.py
@brief Core blind auction logic (no input/output).
"""
def add_bid(bids: dict[str, float], name: str, amount: float) -> None:
"""! @brief Record a bid, replacing any earlier bid by the same bidder.
@param bids Dictionary mapping bidder name to bid amount (modified in place).
@param name Name of the bidder.
@param amount Bid amount; must not be negative.
@throws ValueError If the amount is negative.
"""
if amount < 0:
raise ValueError("Bid amount must not be negative.")
bids[name] = amount
def find_winner(bids: dict[str, float]) -> tuple[str, float]:
"""! @brief Find the highest bidder.
@param bids Dictionary mapping bidder name to bid amount.
@return Tuple of (winner name, winning amount). The first bidder wins ties.
@throws ValueError If there are no bids.
"""
if not bids:
raise ValueError("No bids have been placed.")
winner = max(bids, key=bids.__getitem__)
return winner, bids[winner]
+33
View File
@@ -0,0 +1,33 @@
"""! @file constants.py
@brief Constants used by the Secret Auction program.
"""
## Number of blank lines printed to hide previous bidders' entries.
CLEAR_SCREEN_LINES = 100
## Answers accepted as "yes" when asking for more bidders.
YES_ANSWERS = ("yes", "y")
## Answers accepted as "no" when asking for more bidders.
NO_ANSWERS = ("no", "n")
## Prompt asking for the bidder's name.
PROMPT_NAME = "What is your name? "
## Prompt asking for the bid amount.
PROMPT_BID = "What is your bid? $"
## Prompt asking whether more bidders exist.
PROMPT_MORE_BIDDERS = "Are there any other bidders? Type 'yes' or 'no'. "
## Message shown when the bid is not a valid number.
MSG_INVALID_BID = "Please enter a valid, non-negative number."
## Message shown when the yes/no answer is not understood.
MSG_INVALID_ANSWER = "Please type 'yes' or 'no'."
## Template for the winner announcement.
MSG_WINNER = "The winner is {name} with a bid of ${amount:.2f}."
## Welcome banner.
MSG_WELCOME = "Welcome to the secret auction program."
+60
View File
@@ -0,0 +1,60 @@
"""! @file main.py
@brief Command line interface for the secret auction program.
"""
import math
from secret_auction import constants
from secret_auction.auction import add_bid, find_winner
def clear_screen() -> None:
"""! @brief Hide previous entries by printing blank lines."""
print("\n" * constants.CLEAR_SCREEN_LINES)
def ask_bid() -> float:
"""! @brief Ask for a bid until a valid, non-negative number is entered.
@return The bid amount.
"""
while True:
try:
amount = float(input(constants.PROMPT_BID))
except ValueError:
print(constants.MSG_INVALID_BID)
continue
if math.isfinite(amount) and amount >= 0:
return amount
print(constants.MSG_INVALID_BID)
def ask_more_bidders() -> bool:
"""! @brief Ask whether there are more bidders until yes/no is answered.
@return True if more bidders exist, False otherwise.
"""
while True:
answer = input(constants.PROMPT_MORE_BIDDERS).strip().lower()
if answer in constants.YES_ANSWERS:
return True
if answer in constants.NO_ANSWERS:
return False
print(constants.MSG_INVALID_ANSWER)
def main() -> None:
"""! @brief Run the blind auction: collect bids, then announce the winner."""
print(constants.MSG_WELCOME)
bids: dict[str, float] = {} # kept outside the loop so bids are not lost
more_bidders = True
while more_bidders:
name = input(constants.PROMPT_NAME).strip()
add_bid(bids, name, ask_bid())
more_bidders = ask_more_bidders()
if more_bidders:
clear_screen()
winner, amount = find_winner(bids)
print(constants.MSG_WINNER.format(name=winner, amount=amount))
if __name__ == "__main__":
main()
View File
+33
View File
@@ -0,0 +1,33 @@
import pytest
from secret_auction.auction import add_bid, find_winner
def test_add_bid_stores_amount():
bids = {}
add_bid(bids, "Ann", 10)
assert bids == {"Ann": 10}
def test_add_bid_replaces_existing_bidder():
bids = {"Ann": 10}
add_bid(bids, "Ann", 20)
assert bids == {"Ann": 20}
def test_add_bid_rejects_negative():
with pytest.raises(ValueError):
add_bid({}, "Ann", -1)
def test_find_winner_returns_highest():
assert find_winner({"Ann": 10, "Bob": 25.5, "Cy": 3}) == ("Bob", 25.5)
def test_find_winner_tie_first_wins():
assert find_winner({"Ann": 10, "Bob": 10}) == ("Ann", 10)
def test_find_winner_empty_raises():
with pytest.raises(ValueError):
find_winner({})
+35
View File
@@ -0,0 +1,35 @@
from secret_auction import constants
from secret_auction.main import ask_bid, ask_more_bidders, main
def feed(monkeypatch, answers):
it = iter(answers)
monkeypatch.setattr("builtins.input", lambda _prompt="": next(it))
def test_ask_bid_reprompts_on_invalid(monkeypatch, capsys):
feed(monkeypatch, ["abc", "-5", "nan", "12.5"])
assert ask_bid() == 12.5
assert capsys.readouterr().out.count(constants.MSG_INVALID_BID) == 3
def test_ask_more_bidders(monkeypatch, capsys):
feed(monkeypatch, ["maybe", "Y"])
assert ask_more_bidders() is True
assert constants.MSG_INVALID_ANSWER in capsys.readouterr().out
feed(monkeypatch, ["no"])
assert ask_more_bidders() is False
def test_main_announces_highest_bidder_and_clears_screen(monkeypatch, capsys):
feed(monkeypatch, ["Ann", "10", "yes", "Bob", "25.5", "yes", "Cy", "3", "no"])
main()
out = capsys.readouterr().out
assert "The winner is Bob with a bid of $25.50." in out
assert out.count("\n" * constants.CLEAR_SCREEN_LINES) == 2
def test_main_single_bidder(monkeypatch, capsys):
feed(monkeypatch, ["Ann", "7", "n"])
main()
assert "The winner is Ann with a bid of $7.00." in capsys.readouterr().out