9 Commits

Author SHA1 Message Date
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 99 additions and 30 deletions

View File

@@ -1,6 +1,6 @@
# Lottery TUI # 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) [![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 - 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) [![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"] groups = ["default"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:842afa14523f463c1a73e53c7aeb6d697673d95a2db9adbf935807b1fe5d021a" content_hash = "sha256:6cd4ed6668a18d93170023df0e7cf183ac36d04df220f4dcb4c091eb6623b65f"
[[metadata.targets]] [[metadata.targets]]
requires_python = "==3.13.*" 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]] [[package]]
name = "linkify-it-py" name = "linkify-it-py"
version = "2.0.3" version = "2.0.3"
@@ -25,6 +37,23 @@ files = [
{file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, {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]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "4.0.0" 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.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"},
{file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, {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] [project]
name = "lottery-tui" name = "lottery-tui"
version = "0.2.0" version = "0.2.5"
description = "A terminal user interface for lottery games." description = "A terminal user interface for lottery games."
authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }] 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" requires-python = ">=3.10"
readme = "README.md" readme = "README.md"
license = { text = "MIT" } 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] [project.scripts]
lottery-tui = "lottery_tui.tui:main" lottery-tui = "lottery_tui.tui:main"

View File

@@ -12,18 +12,18 @@ class Result(NamedTuple):
def __str__(self) -> str: def __str__(self) -> str:
"""Return a string representation of the lottery result.""" """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: if self.bonus:
match self.kind: match self.kind:
case 'EuroMillions': case 'EuroMillions':
bonus_name = 'Lucky Stars' bonus_name = 'Lucky Stars'
case 'Set For Life': case 'SetForLife':
bonus_name = 'Life Ball' bonus_name = 'Life Ball'
case 'Thunderball': case 'Thunderball':
bonus_name = 'Thunderball' bonus_name = 'Thunderball'
case _: case _:
bonus_name = 'Bonus Numbers' 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 return out
@@ -40,24 +40,24 @@ class Lottery(ABC):
"""An abstract base class for different types of lotteries.""" """An abstract base class for different types of lotteries."""
@abstractmethod @abstractmethod
def draw(self): def draw(self) -> Result:
"""Perform a lottery draw.""" """Perform a lottery draw."""
@register_lottery @register_lottery
class UKlotto(Lottery): class UKLotto(Lottery):
"""A class representing the UK Lotto lottery. """A class representing the UK Lotto lottery.
Uk Lotto draws 6 numbers from a pool of 1 to 59, without replacement. UK Lotto draws 6 numbers from a pool of 1 to 59, without replacement.
There is no bonus number in UK Lotto. There is no bonus number in UK Lotto.
""" """
POSSIBLE_NUMBERS = range(1, 60) POSSIBLE_NUMBERS = range(1, 60)
def draw(self): def draw(self) -> Result:
"""Perform a UK Lotto draw.""" """Perform a UK Lotto draw."""
result = random.sample(UKlotto.POSSIBLE_NUMBERS, 6) result = random.sample(UKLotto.POSSIBLE_NUMBERS, 6)
return Result(kind='UK Lotto', numbers=result, bonus=None) return Result(kind=type(self).__name__, numbers=result, bonus=None)
@register_lottery @register_lottery
@@ -71,11 +71,11 @@ class EuroMillions(Lottery):
POSSIBLE_NUMBERS = range(1, 51) POSSIBLE_NUMBERS = range(1, 51)
POSSIBLE_BONUS_NUMBERS = range(1, 13) POSSIBLE_BONUS_NUMBERS = range(1, 13)
def draw(self): def draw(self) -> Result:
"""Perform a EuroMillions draw.""" """Perform a EuroMillions draw."""
numbers = random.sample(EuroMillions.POSSIBLE_NUMBERS, 5) numbers = random.sample(EuroMillions.POSSIBLE_NUMBERS, 5)
bonus = random.sample(EuroMillions.POSSIBLE_BONUS_NUMBERS, 2) 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 @register_lottery
@@ -83,16 +83,16 @@ class SetForLife(Lottery):
"""A class representing the Set For Life lottery. """A class representing the Set For Life lottery.
Set For Life draws 5 numbers from a pool of 1 to 39, without replacement, 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) POSSIBLE_NUMBERS = range(1, 40)
def draw(self): def draw(self) -> Result:
"""Perform a Set For Life draw.""" """Perform a Set For Life draw."""
numbers = random.sample(SetForLife.POSSIBLE_NUMBERS, 5) numbers = random.sample(SetForLife.POSSIBLE_NUMBERS, 5)
life_ball = [random.randint(1, 10)] 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 @register_lottery
@@ -100,16 +100,16 @@ class Thunderball(Lottery):
"""A class representing the Thunderball lottery. """A class representing the Thunderball lottery.
Thunderball draws 5 numbers from a pool of 1 to 39, without replacement, 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.""" """Perform a Thunderball draw."""
numbers = random.sample(Thunderball.POSSIBLE_NUMBERS, 5) numbers = random.sample(Thunderball.POSSIBLE_NUMBERS, 5)
thunderball = [random.randint(1, 14)] 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: 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.app import App, ComposeResult
from textual.containers import Container from textual.containers import Container
from textual.widgets import Button, Label, Select, Static from textual.widgets import Button, Label, Select, Static
@@ -37,15 +39,31 @@ class LotteryTUI(App):
def on_button_pressed(self, event): def on_button_pressed(self, event):
"""Handle button press events.""" """Handle button press events."""
if event.button.id == 'draw-button': 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 selected_lottery = self.query_one('#lottery-select').value
try: try:
lottery_obj = request_lottery_obj(selected_lottery) lottery_obj = request_lottery_obj(selected_lottery)
result = lottery_obj.draw() except ValueError:
self.query_one('#result-label').update(f'Result: {result}') ERR_MSG = f'Invalid lottery selection: {selected_lottery}'
except ValueError as e: logger.exception(ERR_MSG)
self.query_one('#result-label').update(str(e)) 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(): def main():