10 Commits

Author SHA1 Message Date
8b743abcfb rename UK Lotto to Lotto to match the website.
patch  bump
2026-02-22 23:34:10 +00:00
56028b2c52 get Result kind name from class type, it's more flexible to change.
patch bump
2026-02-22 23:24:36 +00:00
67b3887bc8 should we get a ValueError when requesting a lottery object allow it to bubble up.
patch bump
2026-02-22 14:33:48 +00:00
2e0052b5fe upd docstrings 2026-02-22 13:29:06 +00:00
a986348168 add python version classifiers
patch bump
2026-02-21 22:57:25 +00:00
bccf1b1ad2 upd pdm badge 2026-02-21 22:16:14 +00:00
3ecfb84a87 remove comment 2026-02-21 22:12:55 +00:00
039487cfc5 sort the numbers before printing them
upd screenshot to reflect sorted results

patch bump
2026-02-21 22:08:45 +00:00
84fcbd326a add annotations 2026-02-21 22:03:52 +00:00
47ced52722 check for empty selection and return early if blank.
this fixes a crash should a draw be attempted without a selection.
2026-02-21 21:56:05 +00:00
6 changed files with 103 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
# Lottery TUI
[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev)
[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![PyPI - Version](https://img.shields.io/pypi/v/lottery-tui.svg)](https://pypi.org/project/lottery-tui)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lottery-tui.svg)](https://pypi.org/project/lottery-tui)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 23 KiB

43
pdm.lock generated
View File

@@ -5,11 +5,23 @@
groups = ["default"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:842afa14523f463c1a73e53c7aeb6d697673d95a2db9adbf935807b1fe5d021a"
content_hash = "sha256:6cd4ed6668a18d93170023df0e7cf183ac36d04df220f4dcb4c091eb6623b65f"
[[metadata.targets]]
requires_python = "==3.13.*"
[[package]]
name = "colorama"
version = "0.4.6"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Cross-platform colored terminal text."
groups = ["default"]
marker = "sys_platform == \"win32\" and python_version == \"3.13\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "linkify-it-py"
version = "2.0.3"
@@ -25,6 +37,23 @@ files = [
{file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"},
]
[[package]]
name = "loguru"
version = "0.7.3"
requires_python = "<4.0,>=3.5"
summary = "Python logging made (stupidly) simple"
groups = ["default"]
marker = "python_version == \"3.13\""
dependencies = [
"aiocontextvars>=0.2.0; python_version < \"3.7\"",
"colorama>=0.3.4; sys_platform == \"win32\"",
"win32-setctime>=1.0.0; sys_platform == \"win32\"",
]
files = [
{file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"},
{file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"},
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
@@ -167,3 +196,15 @@ files = [
{file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"},
{file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"},
]
[[package]]
name = "win32-setctime"
version = "1.2.0"
requires_python = ">=3.5"
summary = "A small Python utility to set file creation time on Windows"
groups = ["default"]
marker = "sys_platform == \"win32\" and python_version == \"3.13\""
files = [
{file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"},
{file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"},
]

View File

@@ -1,12 +1,22 @@
[project]
name = "lottery-tui"
version = "0.2.0"
version = "0.2.6"
description = "A terminal user interface for lottery games."
authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }]
dependencies = ["textual>=8.0.0"]
dependencies = ["textual>=8.0.0", "loguru>=0.7.3"]
requires-python = ">=3.10"
readme = "README.md"
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
[project.scripts]
lottery-tui = "lottery_tui.tui:main"

View File

@@ -12,7 +12,7 @@ class Result(NamedTuple):
def __str__(self) -> str:
"""Return a string representation of the lottery result."""
out = f'Numbers: {", ".join(str(n) for n in self.numbers)}'
out = f'Numbers: {", ".join(str(n) for n in sorted(self.numbers))}'
if self.bonus:
match self.kind:
case 'EuroMillions':
@@ -23,7 +23,7 @@ class Result(NamedTuple):
bonus_name = 'Thunderball'
case _:
bonus_name = 'Bonus Numbers'
out += f'\n{bonus_name}: {", ".join(str(n) for n in self.bonus)}'
out += f'\n{bonus_name}: {", ".join(str(n) for n in sorted(self.bonus))}'
return out
@@ -40,24 +40,24 @@ class Lottery(ABC):
"""An abstract base class for different types of lotteries."""
@abstractmethod
def draw(self):
def draw(self) -> Result:
"""Perform a lottery draw."""
@register_lottery
class UKlotto(Lottery):
"""A class representing the UK Lotto lottery.
class Lotto(Lottery):
"""A class representing the Lotto lottery.
Uk Lotto draws 6 numbers from a pool of 1 to 59, without replacement.
There is no bonus number in UK Lotto.
Lotto draws 6 numbers from a pool of 1 to 59, without replacement.
There is no bonus number in Lotto.
"""
POSSIBLE_NUMBERS = range(1, 60)
def draw(self):
"""Perform a UK Lotto draw."""
result = random.sample(UKlotto.POSSIBLE_NUMBERS, 6)
return Result(kind='UK Lotto', numbers=result, bonus=None)
def draw(self) -> Result:
"""Perform a Lotto draw."""
result = random.sample(Lotto.POSSIBLE_NUMBERS, 6)
return Result(kind=type(self).__name__, numbers=result, bonus=None)
@register_lottery
@@ -71,11 +71,11 @@ class EuroMillions(Lottery):
POSSIBLE_NUMBERS = range(1, 51)
POSSIBLE_BONUS_NUMBERS = range(1, 13)
def draw(self):
def draw(self) -> Result:
"""Perform a EuroMillions draw."""
numbers = random.sample(EuroMillions.POSSIBLE_NUMBERS, 5)
bonus = random.sample(EuroMillions.POSSIBLE_BONUS_NUMBERS, 2)
return Result(kind='EuroMillions', numbers=numbers, bonus=bonus)
return Result(kind=type(self).__name__, numbers=numbers, bonus=bonus)
@register_lottery
@@ -83,16 +83,16 @@ class SetForLife(Lottery):
"""A class representing the Set For Life lottery.
Set For Life draws 5 numbers from a pool of 1 to 39, without replacement,
and 1 "Life Ball" number from a separate pool of 1 to 10, also without replacement.
and 1 "Life Ball" number from a separate pool of 1 to 10.
"""
POSSIBLE_NUMBERS = range(1, 40)
def draw(self):
def draw(self) -> Result:
"""Perform a Set For Life draw."""
numbers = random.sample(SetForLife.POSSIBLE_NUMBERS, 5)
life_ball = [random.randint(1, 10)]
return Result(kind='Set For Life', numbers=numbers, bonus=life_ball)
return Result(kind=type(self).__name__, numbers=numbers, bonus=life_ball)
@register_lottery
@@ -100,16 +100,16 @@ class Thunderball(Lottery):
"""A class representing the Thunderball lottery.
Thunderball draws 5 numbers from a pool of 1 to 39, without replacement,
and 1 "Thunderball" number from a separate pool of 1 to 14, also without replacement.
and 1 "Thunderball" number from a separate pool of 1 to 14.
"""
POSSIBLE_NUMBERS = range(1, 40) # Thunderball numbers range from 1 to 39
POSSIBLE_NUMBERS = range(1, 40)
def draw(self):
def draw(self) -> Result:
"""Perform a Thunderball draw."""
numbers = random.sample(Thunderball.POSSIBLE_NUMBERS, 5)
thunderball = [random.randint(1, 14)]
return Result(kind='Thunderball', numbers=numbers, bonus=thunderball)
return Result(kind=type(self).__name__, numbers=numbers, bonus=thunderball)
def request_lottery_obj(lottery_name: str) -> Lottery:

View File

@@ -1,3 +1,5 @@
from loguru import logger
from rich.text import Text
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.widgets import Button, Label, Select, Static
@@ -17,7 +19,7 @@ class LotteryTUI(App):
Static('Pick a lottery to play:', id='instructions'),
Select(
options=[
('UK Lotto', 'uklotto'),
('Lotto', 'lotto'),
('EuroMillions', 'euromillions'),
('Set For Life', 'setforlife'),
('Thunderball', 'thunderball'),
@@ -37,15 +39,31 @@ class LotteryTUI(App):
def on_button_pressed(self, event):
"""Handle button press events."""
if event.button.id == 'draw-button':
self._draw_button_handler()
def _draw_button_handler(self):
"""Handle the draw button press."""
if self.query_one('#lottery-select').is_blank():
self._update_result_label(
Text('Please select a lottery before drawing.', style='bold #ff8c42')
)
return
selected_lottery = self.query_one('#lottery-select').value
try:
lottery_obj = request_lottery_obj(selected_lottery)
result = lottery_obj.draw()
self.query_one('#result-label').update(f'Result: {result}')
except ValueError as e:
self.query_one('#result-label').update(str(e))
except ValueError:
ERR_MSG = f'Invalid lottery selection: {selected_lottery}'
logger.exception(ERR_MSG)
raise
self.query_one('#result-label').update(str(result))
result = lottery_obj.draw()
self._update_result_label(str(result))
def _update_result_label(self, message: str):
"""Update the result label with a new message."""
self.query_one('#result-label').update(message)
def main():