lucky-cli 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lucky/__init__.py +0 -0
- lucky/cli.py +48 -0
- lucky/commands/__init__.py +0 -0
- lucky/commands/check.py +50 -0
- lucky/commands/pick.py +28 -0
- lucky/commands/show.py +55 -0
- lucky/config.py +9 -0
- lucky/console/__init__.py +0 -0
- lucky/console/rich.py +129 -0
- lucky/database/__init__.py +0 -0
- lucky/database/db.py +19 -0
- lucky/database/models.py +34 -0
- lucky/database/repository.py +57 -0
- lucky/lottery/__init__.py +1 -0
- lucky/lottery/base.py +57 -0
- lucky/lottery/checker.py +23 -0
- lucky/lottery/generator.py +16 -0
- lucky/lottery/mega.py +12 -0
- lucky/lottery/powerball.py +12 -0
- lucky/lottery/stats.py +48 -0
- lucky/updater/__init__.py +0 -0
- lucky/updater/client.py +7 -0
- lucky/updater/fixtures/mega-millions.json +4 -0
- lucky/updater/fixtures/powerball.json +5 -0
- lucky/updater/sources.py +35 -0
- lucky/updater/updater.py +32 -0
- lucky_cli-0.0.3.dist-info/METADATA +141 -0
- lucky_cli-0.0.3.dist-info/RECORD +34 -0
- lucky_cli-0.0.3.dist-info/WHEEL +5 -0
- lucky_cli-0.0.3.dist-info/entry_points.txt +2 -0
- lucky_cli-0.0.3.dist-info/licenses/LICENSE +21 -0
- lucky_cli-0.0.3.dist-info/scm_file_list.json +58 -0
- lucky_cli-0.0.3.dist-info/scm_version.json +8 -0
- lucky_cli-0.0.3.dist-info/top_level.txt +1 -0
lucky/__init__.py
ADDED
|
File without changes
|
lucky/cli.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError
|
|
2
|
+
from importlib.metadata import version as pkg_version
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
import lucky.lottery # noqa: F401 -- side effect: registers powerball + mega-millions
|
|
7
|
+
|
|
8
|
+
from .commands.check import check
|
|
9
|
+
from .commands.pick import pick
|
|
10
|
+
from .commands.show import show
|
|
11
|
+
from .database.db import get_session
|
|
12
|
+
from .updater.updater import sync_all
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(no_args_is_help=True, add_completion=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _version_callback(value: bool) -> None:
|
|
18
|
+
if not value:
|
|
19
|
+
return
|
|
20
|
+
try:
|
|
21
|
+
v = pkg_version("lucky-cli")
|
|
22
|
+
except PackageNotFoundError:
|
|
23
|
+
v = "0.0.0+dev"
|
|
24
|
+
typer.echo(v)
|
|
25
|
+
raise typer.Exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback()
|
|
29
|
+
def main(
|
|
30
|
+
version: bool = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--version",
|
|
33
|
+
callback=_version_callback,
|
|
34
|
+
is_eager=True,
|
|
35
|
+
help="Show the installed version and exit.",
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""lucky — lottery ticket generator, checker, and stats CLI."""
|
|
39
|
+
session = get_session()
|
|
40
|
+
try:
|
|
41
|
+
sync_all(session)
|
|
42
|
+
finally:
|
|
43
|
+
session.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
app.command()(pick)
|
|
47
|
+
app.command()(check)
|
|
48
|
+
app.command()(show)
|
|
File without changes
|
lucky/commands/check.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..console.rich import fail, render_check_result
|
|
6
|
+
from ..database import repository
|
|
7
|
+
from ..database.db import get_session
|
|
8
|
+
from ..lottery.base import game_metavar, get_game
|
|
9
|
+
from ..lottery.checker import check_ticket
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check(
|
|
13
|
+
game: Annotated[
|
|
14
|
+
str,
|
|
15
|
+
typer.Argument(
|
|
16
|
+
metavar=game_metavar(),
|
|
17
|
+
help="Game slug",
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
numbers: Annotated[list[int], typer.Argument(help="Your main numbers")],
|
|
21
|
+
special: Annotated[list[int], typer.Option("--special", help="Your special number(s)")] = [], # noqa: B006
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Check NUMBERS against the latest draw for GAME."""
|
|
24
|
+
try:
|
|
25
|
+
resolved_game = get_game(game)
|
|
26
|
+
except ValueError as exc:
|
|
27
|
+
fail(str(exc))
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
resolved_game.validate_ticket(numbers, special)
|
|
32
|
+
except ValueError as exc:
|
|
33
|
+
usage = (
|
|
34
|
+
f"Usage: lucky check {resolved_game.slug} "
|
|
35
|
+
f"<{resolved_game.main_count} main numbers> "
|
|
36
|
+
f"--special <{resolved_game.special_count} number(s)>"
|
|
37
|
+
)
|
|
38
|
+
fail(f"{exc}\n{usage}")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
session = get_session()
|
|
42
|
+
draw = repository.latest_draw(session, resolved_game.slug)
|
|
43
|
+
if draw is None:
|
|
44
|
+
session.close()
|
|
45
|
+
fail(f"No draw data available for {resolved_game.slug} yet.")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
result = check_ticket(numbers, special, draw.main, draw.special)
|
|
49
|
+
render_check_result(numbers, special, draw.main, draw.special, result)
|
|
50
|
+
session.close()
|
lucky/commands/pick.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..console.rich import fail, render_tickets
|
|
6
|
+
from ..lottery.base import game_metavar, get_game
|
|
7
|
+
from ..lottery.generator import generate_ticket
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pick(
|
|
11
|
+
game: Annotated[
|
|
12
|
+
str,
|
|
13
|
+
typer.Argument(
|
|
14
|
+
metavar=game_metavar(),
|
|
15
|
+
help="Game slug",
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
count: Annotated[int, typer.Option("--count", min=1, help="Number of tickets to generate")] = 1,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Generate one or more valid random tickets for GAME."""
|
|
21
|
+
try:
|
|
22
|
+
resolved_game = get_game(game)
|
|
23
|
+
except ValueError as exc:
|
|
24
|
+
fail(str(exc))
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
tickets = [generate_ticket(resolved_game) for _ in range(count)]
|
|
28
|
+
render_tickets(resolved_game.name, tickets)
|
lucky/commands/show.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..console.rich import fail, render_draw_panel, render_history_table, render_stats
|
|
6
|
+
from ..database import repository
|
|
7
|
+
from ..database.db import get_session
|
|
8
|
+
from ..lottery.base import game_metavar, get_game
|
|
9
|
+
from ..lottery.stats import compute_stats
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def show(
|
|
13
|
+
game: Annotated[
|
|
14
|
+
str,
|
|
15
|
+
typer.Argument(
|
|
16
|
+
metavar=game_metavar(),
|
|
17
|
+
help="Game slug",
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
history: Annotated[bool, typer.Option("--history", help="Show draw history")] = False,
|
|
21
|
+
stats: Annotated[bool, typer.Option("--stats", help="Show number frequency stats")] = False,
|
|
22
|
+
limit: Annotated[int, typer.Option("--limit", min=1, help="Number of draws to show in history")] = 10,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Show the latest draw for GAME, or its --history / --stats."""
|
|
25
|
+
if history and stats:
|
|
26
|
+
fail("--history and --stats are mutually exclusive")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
resolved_game = get_game(game)
|
|
31
|
+
except ValueError as exc:
|
|
32
|
+
fail(str(exc))
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
session = get_session()
|
|
36
|
+
|
|
37
|
+
if stats:
|
|
38
|
+
draws = repository.all_draws(session, resolved_game.slug)
|
|
39
|
+
render_stats(resolved_game.name, compute_stats(draws))
|
|
40
|
+
session.close()
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if history:
|
|
44
|
+
draws = repository.history(session, resolved_game.slug, limit)
|
|
45
|
+
render_history_table(resolved_game.name, draws)
|
|
46
|
+
session.close()
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
draw = repository.latest_draw(session, resolved_game.slug)
|
|
50
|
+
if draw is None:
|
|
51
|
+
session.close()
|
|
52
|
+
fail(f"No draw data available for {resolved_game.slug} yet.")
|
|
53
|
+
return
|
|
54
|
+
render_draw_panel(resolved_game.name, draw)
|
|
55
|
+
session.close()
|
lucky/config.py
ADDED
|
File without changes
|
lucky/console/rich.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterator
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import NoReturn
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.progress import Progress
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def print_error(message: str) -> None:
|
|
15
|
+
console.print(f"[bold red]Error:[/bold red] {message}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fail(message: str) -> NoReturn:
|
|
19
|
+
print_error(message)
|
|
20
|
+
raise typer.Exit(code=1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@contextmanager
|
|
24
|
+
def update_progress(label: str, total: int) -> Iterator[Callable[[], None]]:
|
|
25
|
+
with Progress(console=console) as progress:
|
|
26
|
+
task_id = progress.add_task(f"Updating {label}...", total=total)
|
|
27
|
+
|
|
28
|
+
def advance() -> None:
|
|
29
|
+
progress.update(task_id, advance=1)
|
|
30
|
+
|
|
31
|
+
yield advance
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_tickets(game_name: str, tickets) -> None:
|
|
35
|
+
table = Table(title=f"{game_name} — Picks")
|
|
36
|
+
table.add_column("#")
|
|
37
|
+
table.add_column("Main Numbers")
|
|
38
|
+
table.add_column("Special")
|
|
39
|
+
for i, ticket in enumerate(tickets, start=1):
|
|
40
|
+
table.add_row(
|
|
41
|
+
str(i),
|
|
42
|
+
", ".join(map(str, ticket.main)),
|
|
43
|
+
", ".join(map(str, ticket.special)) or "—",
|
|
44
|
+
)
|
|
45
|
+
console.print(table)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render_draw_panel(game_name: str, draw) -> None:
|
|
49
|
+
body = (
|
|
50
|
+
f"Date: {draw.date}\n"
|
|
51
|
+
f"Numbers: {', '.join(map(str, draw.main))}\n"
|
|
52
|
+
f"Special: {', '.join(map(str, draw.special)) or 'N/A'}\n"
|
|
53
|
+
f"Jackpot: {draw.jackpot or 'N/A'}"
|
|
54
|
+
)
|
|
55
|
+
console.print(Panel(body, title=f"{game_name} — Latest Draw"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def render_history_table(game_name: str, draws) -> None:
|
|
59
|
+
table = Table(title=f"{game_name} — History")
|
|
60
|
+
table.add_column("Date")
|
|
61
|
+
table.add_column("Main Numbers")
|
|
62
|
+
table.add_column("Special")
|
|
63
|
+
table.add_column("Jackpot")
|
|
64
|
+
for draw in draws:
|
|
65
|
+
table.add_row(
|
|
66
|
+
draw.date,
|
|
67
|
+
", ".join(map(str, draw.main)),
|
|
68
|
+
", ".join(map(str, draw.special)) or "—",
|
|
69
|
+
draw.jackpot or "—",
|
|
70
|
+
)
|
|
71
|
+
console.print(table)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def render_stats(game_name: str, stats) -> None:
|
|
75
|
+
table = Table(title=f"{game_name} — Number Frequency")
|
|
76
|
+
table.add_column("Number")
|
|
77
|
+
table.add_column("Frequency")
|
|
78
|
+
for number, freq in sorted(stats.main_frequency.items()):
|
|
79
|
+
table.add_row(str(number), str(freq))
|
|
80
|
+
console.print(table)
|
|
81
|
+
|
|
82
|
+
if stats.special_frequency:
|
|
83
|
+
special_table = Table(title=f"{game_name} — Special Number Frequency")
|
|
84
|
+
special_table.add_column("Number")
|
|
85
|
+
special_table.add_column("Frequency")
|
|
86
|
+
for number, freq in sorted(stats.special_frequency.items()):
|
|
87
|
+
special_table.add_row(str(number), str(freq))
|
|
88
|
+
console.print(special_table)
|
|
89
|
+
|
|
90
|
+
console.print(f"[green]Hot:[/green] {stats.hot_main}")
|
|
91
|
+
console.print(f"[blue]Cold:[/blue] {stats.cold_main}")
|
|
92
|
+
console.print(f"Odd/Even: {stats.odd_count}/{stats.even_count}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def render_check_result(your_main, your_special, draw_main, draw_special, result) -> None:
|
|
96
|
+
your_main_sorted = sorted(your_main)
|
|
97
|
+
draw_main_sorted = sorted(draw_main)
|
|
98
|
+
your_special_sorted = sorted(your_special)
|
|
99
|
+
draw_special_sorted = sorted(draw_special)
|
|
100
|
+
|
|
101
|
+
your_main_set = set(your_main)
|
|
102
|
+
your_special_set = set(your_special)
|
|
103
|
+
draw_main_set = set(draw_main)
|
|
104
|
+
draw_special_set = set(draw_special)
|
|
105
|
+
|
|
106
|
+
def fmt(value: int, hit: bool) -> str:
|
|
107
|
+
return f"[bold green]{value}[/bold green]" if hit else str(value)
|
|
108
|
+
|
|
109
|
+
table = Table(title="Ticket Comparison")
|
|
110
|
+
table.add_column("")
|
|
111
|
+
for i in range(len(your_main_sorted)):
|
|
112
|
+
table.add_column(f"Main {i + 1}")
|
|
113
|
+
for i in range(len(your_special_sorted)):
|
|
114
|
+
table.add_column(f"Special {i + 1}")
|
|
115
|
+
|
|
116
|
+
table.add_row(
|
|
117
|
+
"Your Numbers",
|
|
118
|
+
*[fmt(n, n in draw_main_set) for n in your_main_sorted],
|
|
119
|
+
*[fmt(n, n in draw_special_set) for n in your_special_sorted],
|
|
120
|
+
)
|
|
121
|
+
table.add_row(
|
|
122
|
+
"Winning Numbers",
|
|
123
|
+
*[fmt(n, n in your_main_set) for n in draw_main_sorted],
|
|
124
|
+
*[fmt(n, n in your_special_set) for n in draw_special_sorted],
|
|
125
|
+
)
|
|
126
|
+
console.print(table)
|
|
127
|
+
|
|
128
|
+
console.print(f"Main matches: {result.main_matches}/{result.main_count}")
|
|
129
|
+
console.print(f"Special matches: {result.special_matches}/{result.special_count}")
|
|
File without changes
|
lucky/database/db.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from sqlalchemy import create_engine
|
|
2
|
+
from sqlalchemy.engine import Engine
|
|
3
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
4
|
+
|
|
5
|
+
from ..config import get_db_path
|
|
6
|
+
from .models import Base
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_engine() -> Engine:
|
|
10
|
+
db_path = get_db_path()
|
|
11
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
engine = create_engine(f"sqlite:///{db_path}")
|
|
13
|
+
Base.metadata.create_all(engine)
|
|
14
|
+
return engine
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_session() -> Session:
|
|
18
|
+
factory = sessionmaker(bind=get_engine())
|
|
19
|
+
return factory()
|
lucky/database/models.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import TEXT, String, UniqueConstraint
|
|
4
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
5
|
+
from sqlalchemy.types import TypeDecorator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Base(DeclarativeBase):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JSONIntList(TypeDecorator):
|
|
13
|
+
"""Stores a list[int] as a JSON-encoded TEXT column."""
|
|
14
|
+
|
|
15
|
+
impl = TEXT
|
|
16
|
+
cache_ok = True
|
|
17
|
+
|
|
18
|
+
def process_bind_param(self, value, dialect):
|
|
19
|
+
return json.dumps(value if value is not None else [])
|
|
20
|
+
|
|
21
|
+
def process_result_value(self, value, dialect):
|
|
22
|
+
return json.loads(value) if value is not None else []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Draw(Base):
|
|
26
|
+
__tablename__ = "draws"
|
|
27
|
+
__table_args__ = (UniqueConstraint("game", "date", name="uq_draws_game_date"),)
|
|
28
|
+
|
|
29
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
30
|
+
game: Mapped[str] = mapped_column(String, nullable=False)
|
|
31
|
+
date: Mapped[str] = mapped_column(String, nullable=False)
|
|
32
|
+
main: Mapped[list[int]] = mapped_column(JSONIntList, nullable=False)
|
|
33
|
+
special: Mapped[list[int]] = mapped_column(JSONIntList, nullable=False)
|
|
34
|
+
jackpot: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from sqlalchemy import select
|
|
2
|
+
from sqlalchemy.orm import Session
|
|
3
|
+
|
|
4
|
+
from .models import Draw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def latest_draw(session: Session, game: str) -> Draw | None:
|
|
8
|
+
stmt = select(Draw).where(Draw.game == game).order_by(Draw.date.desc()).limit(1)
|
|
9
|
+
return session.scalars(stmt).first()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def history(session: Session, game: str, limit: int) -> list[Draw]:
|
|
13
|
+
stmt = select(Draw).where(Draw.game == game).order_by(Draw.date.desc()).limit(limit)
|
|
14
|
+
return list(session.scalars(stmt))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def all_draws(session: Session, game: str) -> list[Draw]:
|
|
18
|
+
stmt = select(Draw).where(Draw.game == game).order_by(Draw.date.asc())
|
|
19
|
+
return list(session.scalars(stmt))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def existing_dates(session: Session, game: str) -> set[str]:
|
|
23
|
+
stmt = select(Draw.date).where(Draw.game == game)
|
|
24
|
+
return set(session.scalars(stmt))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_draw(session: Session, game: str, record) -> None:
|
|
28
|
+
session.add(
|
|
29
|
+
Draw(
|
|
30
|
+
game=game,
|
|
31
|
+
date=record.date,
|
|
32
|
+
main=record.main,
|
|
33
|
+
special=record.special,
|
|
34
|
+
jackpot=record.jackpot,
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def filter_new_records(session: Session, game: str, records: list) -> list:
|
|
40
|
+
existing = existing_dates(session, game)
|
|
41
|
+
seen: set[str] = set()
|
|
42
|
+
new_records = []
|
|
43
|
+
for r in records:
|
|
44
|
+
if r.date in existing or r.date in seen:
|
|
45
|
+
continue
|
|
46
|
+
seen.add(r.date)
|
|
47
|
+
new_records.append(r)
|
|
48
|
+
return new_records
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def upsert_draws(session: Session, game: str, records: list) -> int:
|
|
52
|
+
new_records = filter_new_records(session, game, records)
|
|
53
|
+
for record in new_records:
|
|
54
|
+
add_draw(session, game, record)
|
|
55
|
+
if new_records:
|
|
56
|
+
session.commit()
|
|
57
|
+
return len(new_records)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import mega, powerball # noqa: F401 -- side effect: registers both games
|
lucky/lottery/base.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class Game:
|
|
6
|
+
slug: str
|
|
7
|
+
name: str
|
|
8
|
+
main_count: int
|
|
9
|
+
main_range: tuple[int, int]
|
|
10
|
+
special_count: int
|
|
11
|
+
special_range: tuple[int, int] | None
|
|
12
|
+
|
|
13
|
+
def validate_ticket(self, main: list[int], special: list[int]) -> None:
|
|
14
|
+
if len(main) != self.main_count:
|
|
15
|
+
raise ValueError(
|
|
16
|
+
f"{self.name} requires exactly {self.main_count} main numbers, got {len(main)}"
|
|
17
|
+
)
|
|
18
|
+
if len(set(main)) != len(main):
|
|
19
|
+
raise ValueError("main numbers must be unique")
|
|
20
|
+
lo, hi = self.main_range
|
|
21
|
+
if any(not (lo <= n <= hi) for n in main):
|
|
22
|
+
raise ValueError(f"main numbers must be in range {lo}-{hi}")
|
|
23
|
+
|
|
24
|
+
if len(special) != self.special_count:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
f"{self.name} requires exactly {self.special_count} special number(s), got {len(special)}"
|
|
27
|
+
)
|
|
28
|
+
if self.special_range is not None:
|
|
29
|
+
slo, shi = self.special_range
|
|
30
|
+
if any(not (slo <= n <= shi) for n in special):
|
|
31
|
+
raise ValueError(f"special numbers must be in range {slo}-{shi}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Ticket:
|
|
36
|
+
main: list[int]
|
|
37
|
+
special: list[int]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
GAMES: dict[str, Game] = {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def register(game: Game) -> Game:
|
|
44
|
+
GAMES[game.slug] = game
|
|
45
|
+
return game
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_game(slug: str) -> Game:
|
|
49
|
+
try:
|
|
50
|
+
return GAMES[slug]
|
|
51
|
+
except KeyError:
|
|
52
|
+
choices = ", ".join(sorted(GAMES)) or "(none registered)"
|
|
53
|
+
raise ValueError(f"Unknown game '{slug}'. Choices: {choices}") from None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def game_metavar() -> str:
|
|
57
|
+
return f"[{'|'.join(sorted(GAMES))}]"
|
lucky/lottery/checker.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class CheckResult:
|
|
6
|
+
main_matches: int
|
|
7
|
+
main_count: int
|
|
8
|
+
special_matches: int
|
|
9
|
+
special_count: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_ticket(
|
|
13
|
+
main: list[int],
|
|
14
|
+
special: list[int],
|
|
15
|
+
draw_main: list[int],
|
|
16
|
+
draw_special: list[int],
|
|
17
|
+
) -> CheckResult:
|
|
18
|
+
return CheckResult(
|
|
19
|
+
main_matches=len(set(main) & set(draw_main)),
|
|
20
|
+
main_count=len(draw_main),
|
|
21
|
+
special_matches=len(set(special) & set(draw_special)),
|
|
22
|
+
special_count=len(draw_special),
|
|
23
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from .base import Game, Ticket
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_ticket(game: Game) -> Ticket:
|
|
7
|
+
main_lo, main_hi = game.main_range
|
|
8
|
+
main = sorted(random.sample(range(main_lo, main_hi + 1), game.main_count))
|
|
9
|
+
|
|
10
|
+
special: list[int] = []
|
|
11
|
+
if game.special_count:
|
|
12
|
+
assert game.special_range is not None
|
|
13
|
+
special_lo, special_hi = game.special_range
|
|
14
|
+
special = sorted(random.sample(range(special_lo, special_hi + 1), game.special_count))
|
|
15
|
+
|
|
16
|
+
return Ticket(main=main, special=special)
|
lucky/lottery/mega.py
ADDED
lucky/lottery/stats.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class GameStats:
|
|
7
|
+
main_frequency: dict[int, int]
|
|
8
|
+
special_frequency: dict[int, int]
|
|
9
|
+
hot_main: list[int]
|
|
10
|
+
cold_main: list[int]
|
|
11
|
+
odd_count: int
|
|
12
|
+
even_count: int
|
|
13
|
+
total_draws: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compute_stats(draws, top_n: int = 5) -> GameStats:
|
|
17
|
+
main_counter: Counter[int] = Counter()
|
|
18
|
+
special_counter: Counter[int] = Counter()
|
|
19
|
+
odd_count = 0
|
|
20
|
+
even_count = 0
|
|
21
|
+
|
|
22
|
+
for draw in draws:
|
|
23
|
+
main_counter.update(draw.main)
|
|
24
|
+
special_counter.update(draw.special)
|
|
25
|
+
for n in draw.main:
|
|
26
|
+
if n % 2 == 0:
|
|
27
|
+
even_count += 1
|
|
28
|
+
else:
|
|
29
|
+
odd_count += 1
|
|
30
|
+
|
|
31
|
+
def ranked(reverse: bool) -> list[int]:
|
|
32
|
+
return [
|
|
33
|
+
number
|
|
34
|
+
for number, _ in sorted(main_counter.items(), key=lambda item: (-item[1] if reverse else item[1], item[0]))
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
hot_main = ranked(reverse=True)[:top_n]
|
|
38
|
+
cold_main = ranked(reverse=False)[:top_n]
|
|
39
|
+
|
|
40
|
+
return GameStats(
|
|
41
|
+
main_frequency=dict(main_counter),
|
|
42
|
+
special_frequency=dict(special_counter),
|
|
43
|
+
hot_main=hot_main,
|
|
44
|
+
cold_main=cold_main,
|
|
45
|
+
odd_count=odd_count,
|
|
46
|
+
even_count=even_count,
|
|
47
|
+
total_draws=len(draws),
|
|
48
|
+
)
|
|
File without changes
|
lucky/updater/client.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"date": "2026-06-14", "main": [12, 18, 33, 44, 61], "special": [9], "jackpot": "$135,000,000"},
|
|
3
|
+
{"date": "2026-06-16", "main": [5, 9, 27, 41, 63], "special": [21], "jackpot": "$142,000,000"},
|
|
4
|
+
{"date": "2026-06-18", "main": [2, 14, 29, 38, 55], "special": [3], "jackpot": "$150,000,000"}
|
|
5
|
+
]
|
lucky/updater/sources.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class DrawRecord:
|
|
9
|
+
date: str
|
|
10
|
+
main: list[int]
|
|
11
|
+
special: list[int]
|
|
12
|
+
jackpot: str | None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DataSource(Protocol):
|
|
16
|
+
def fetch(self, game_slug: str) -> list[DrawRecord]: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FixtureSource:
|
|
20
|
+
"""Reads bundled per-game JSON fixtures as if they were a remote API response."""
|
|
21
|
+
|
|
22
|
+
def fetch(self, game_slug: str) -> list[DrawRecord]:
|
|
23
|
+
resource = files("lucky.updater.fixtures").joinpath(f"{game_slug}.json")
|
|
24
|
+
if not resource.is_file():
|
|
25
|
+
raise FileNotFoundError(f"No fixture data for game '{game_slug}'")
|
|
26
|
+
rows = json.loads(resource.read_text())
|
|
27
|
+
return [
|
|
28
|
+
DrawRecord(
|
|
29
|
+
date=row["date"],
|
|
30
|
+
main=row["main"],
|
|
31
|
+
special=row["special"],
|
|
32
|
+
jackpot=row.get("jackpot"),
|
|
33
|
+
)
|
|
34
|
+
for row in rows
|
|
35
|
+
]
|
lucky/updater/updater.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.orm import Session
|
|
4
|
+
|
|
5
|
+
from ..console.rich import update_progress
|
|
6
|
+
from ..database import repository
|
|
7
|
+
from ..lottery.base import GAMES
|
|
8
|
+
from . import client
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sync_game(session: Session, slug: str) -> int:
|
|
14
|
+
records = client.get_draws(slug)
|
|
15
|
+
new_records = repository.filter_new_records(session, slug, records)
|
|
16
|
+
if not new_records:
|
|
17
|
+
return 0
|
|
18
|
+
|
|
19
|
+
with update_progress(slug, total=len(new_records)) as advance:
|
|
20
|
+
for record in new_records:
|
|
21
|
+
repository.add_draw(session, slug, record)
|
|
22
|
+
advance()
|
|
23
|
+
session.commit()
|
|
24
|
+
return len(new_records)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sync_all(session: Session) -> None:
|
|
28
|
+
for slug in GAMES:
|
|
29
|
+
try:
|
|
30
|
+
sync_game(session, slug)
|
|
31
|
+
except Exception:
|
|
32
|
+
logger.debug("Update failed for %s; falling back to cached data", slug, exc_info=True)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lucky-cli
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: A lottery CLI tool built with Python for fun
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 PyDeployer
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: httpx>=0.28.1
|
|
31
|
+
Requires-Dist: rich>=15.0.0
|
|
32
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
33
|
+
Requires-Dist: typer>=0.26.7
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# lucky-cli
|
|
37
|
+
|
|
38
|
+
A lottery CLI tool built with Python for fun. Installed command: `lucky`.
|
|
39
|
+
|
|
40
|
+
Supports Powerball (5 numbers 1–69 + special 1–26) and Mega Millions (5 numbers 1–70 + special 1–25).
|
|
41
|
+
|
|
42
|
+
## Install (dev)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv sync
|
|
46
|
+
uv run lucky --help
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
| Command | Description |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `lucky pick <game> [--count N]` | Generate N valid random tickets |
|
|
54
|
+
| `lucky check <game> <n1..n5> --special <s>` | Check a ticket against the latest draw |
|
|
55
|
+
| `lucky show <game>` | Show the latest draw (date, numbers, special, jackpot) — default view |
|
|
56
|
+
| `lucky show <game> --history [--limit N]` | Show the last N draws |
|
|
57
|
+
| `lucky show <game> --stats` | Show number frequency, hot/cold numbers, odd/even distribution |
|
|
58
|
+
| `lucky --version` | Show the installed version |
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
lucky check powerball 12 18 33 44 61 --special 9
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
On every run, `lucky` checks for new draws and updates its local SQLite cache (`~/.lucky-cli/lucky.db`) before executing your command. If offline, it silently falls back to the cached data.
|
|
67
|
+
|
|
68
|
+
## Project layout
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
src/lucky/
|
|
72
|
+
├── cli.py # Typer app entrypoint
|
|
73
|
+
├── commands/ # one module per subcommand
|
|
74
|
+
├── lottery/ # game rules + ticket generator
|
|
75
|
+
├── database/ # SQLAlchemy models/repository
|
|
76
|
+
├── updater/ # remote source + sync logic
|
|
77
|
+
└── console/ # Rich rendering helpers
|
|
78
|
+
tests/
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
See [CLAUDE.md](CLAUDE.md) for the full architecture spec.
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv sync # install deps
|
|
87
|
+
uv run pytest # run tests
|
|
88
|
+
uv run lucky <cmd> # exercise the CLI locally
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Contributing
|
|
92
|
+
|
|
93
|
+
- Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`. Use `feat!:` or a `BREAKING CHANGE:` footer for breaking changes.
|
|
94
|
+
- Versioning is automated from git tags (`setuptools-scm`) — never hand-edit a version string.
|
|
95
|
+
- Route all CLI output through `src/lucky/console/`; avoid raw `print()`.
|
|
96
|
+
- Add/update tests under `tests/` for any change to generator, stats, checker, or database logic.
|
|
97
|
+
- CI publishes to PyPI on `v*` tags via `.github/workflows/release.yml`.
|
|
98
|
+
|
|
99
|
+
## Releasing / Versioning
|
|
100
|
+
|
|
101
|
+
Version numbers are never hand-edited — they're derived from git tags via `setuptools-scm`. To cut a release:
|
|
102
|
+
|
|
103
|
+
1. Find the last release tag (skip if this is the first release):
|
|
104
|
+
```bash
|
|
105
|
+
git describe --tags --abbrev=0
|
|
106
|
+
```
|
|
107
|
+
2. Review commits since that tag:
|
|
108
|
+
```bash
|
|
109
|
+
git log <last-tag>..HEAD --oneline
|
|
110
|
+
```
|
|
111
|
+
3. Classify them by [Conventional Commits](https://www.conventionalcommits.org/) prefix to decide the version bump:
|
|
112
|
+
- `feat:` → MINOR
|
|
113
|
+
- `fix:` → PATCH
|
|
114
|
+
- `feat!:` or a `BREAKING CHANGE:` footer → MAJOR
|
|
115
|
+
- (first release with no prior tag: start at `v0.1.0`)
|
|
116
|
+
4. Add a new section to `CHANGELOG.md`, grouped by category:
|
|
117
|
+
```md
|
|
118
|
+
## v0.3.0
|
|
119
|
+
|
|
120
|
+
### Features
|
|
121
|
+
- add stats command
|
|
122
|
+
|
|
123
|
+
### Fixes
|
|
124
|
+
- fix check logic
|
|
125
|
+
```
|
|
126
|
+
5. Commit the changelog update, then create and push the tag:
|
|
127
|
+
```bash
|
|
128
|
+
git add CHANGELOG.md
|
|
129
|
+
git commit -m "chore: release v0.3.0"
|
|
130
|
+
git tag v0.3.0
|
|
131
|
+
git push origin main v0.3.0
|
|
132
|
+
```
|
|
133
|
+
6. Pushing the tag triggers `.github/workflows/release.yml`, which runs the test suite, builds the package, and publishes to PyPI using the `PYPI_API_TOKEN` secret — nothing else to do manually.
|
|
134
|
+
|
|
135
|
+
## Development process
|
|
136
|
+
|
|
137
|
+
This project is built with [Claude Code](https://claude.com/claude-code) using the Superpowers plugin as its development framework — brainstorming a design spec, writing an implementation plan, then executing it task-by-task with subagent-driven TDD and code review before each merge.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
lucky/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
lucky/cli.py,sha256=NUQCRtmyoTlW3xr0x-y-HGzMwEZXL82Np-eH4MVa8B0,1130
|
|
3
|
+
lucky/config.py,sha256=jmXivLKSgE3WWFzPVvmiIZWLdZMeHS5Pcaqtd76YzoY,209
|
|
4
|
+
lucky/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
lucky/commands/check.py,sha256=6lL6NRD3RhKIembtUZlTIMC_jQ__F2R-JWS6s3aM3gE,1530
|
|
6
|
+
lucky/commands/pick.py,sha256=E1UoO4taUEbTs9s55BCjWyx7nM48tcbQ-F7eYMNP9DA,752
|
|
7
|
+
lucky/commands/show.py,sha256=lzQRO0fMhUbnLO4fFY0zjJkCx70En4Ji1hV5JpoHnGE,1700
|
|
8
|
+
lucky/console/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
lucky/console/rich.py,sha256=BC8aSdWmPfsVAC5KeO0BqfgkBq7qig1WFge4OxeeP3Y,4255
|
|
10
|
+
lucky/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
lucky/database/db.py,sha256=f9T8SyDckbUPpgUTeqKV6_cOx4ISSliAQQPp855tiEo,499
|
|
12
|
+
lucky/database/models.py,sha256=AQBHsDPHHinWYdsnEUQBvnj_wA2Q4yMxLmUBZkrHlJA,1108
|
|
13
|
+
lucky/database/repository.py,sha256=mbXasRZnhgIVzEwrBbS3u1M0pZiuWkFq5kHTFgk1tjU,1687
|
|
14
|
+
lucky/lottery/__init__.py,sha256=MT3dhjv9bafpKopFOPSV3UZWTPWt_P91kFUJLdhjWKQ,82
|
|
15
|
+
lucky/lottery/base.py,sha256=DZ8ez4yNbg87zMyydqK81IViN7VbkvP2ga_ww0OdogA,1661
|
|
16
|
+
lucky/lottery/checker.py,sha256=h4x-IVEPyatUxG7zUJehpUVjiOnZNeyHuoQt7aFOhA4,530
|
|
17
|
+
lucky/lottery/generator.py,sha256=UiM7OBrdfdrH7lBsAdGzEEbdN5RXXIfq9ayssa9D8rQ,505
|
|
18
|
+
lucky/lottery/mega.py,sha256=2HZecjIG9LNXXZy6UqyrqNitHQYUvXlCOIzz5oh3lSU,244
|
|
19
|
+
lucky/lottery/powerball.py,sha256=CtnYs-ke5LP78w2CMMHIfusEr0_grX4HjG7EiS2bkNk,232
|
|
20
|
+
lucky/lottery/stats.py,sha256=mm_NvtM4q_OOAwX_6b5K070d5u8jV7vjFn5tMDoGDFY,1289
|
|
21
|
+
lucky/updater/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
lucky/updater/client.py,sha256=iXuSTzKJfOXDKPX_JJ2o_bguqlBUBanmDY6qpNwPdbM,187
|
|
23
|
+
lucky/updater/sources.py,sha256=eerYD3Vtd5sTY9VDihFdrI5Y0VMOh-1RLR4jzPPwv_U,983
|
|
24
|
+
lucky/updater/updater.py,sha256=B5Tf6ET5mZ98c-4z194_HhncB8h8L-9qfkr_gy1k_Wc,897
|
|
25
|
+
lucky/updater/fixtures/mega-millions.json,sha256=iG4wPzMOnNe95T2pncU5s-LAoEcwrLrINaHGFshylZA,199
|
|
26
|
+
lucky/updater/fixtures/powerball.json,sha256=RLsEGahCLeRe4ngJCQf4U8CM3RP13ej4Ly6E_gwoKbU,298
|
|
27
|
+
lucky_cli-0.0.3.dist-info/licenses/LICENSE,sha256=qqF1Zf1Z0TinvV92LGdwefIOPedisg9Eut7nrMFRKms,1067
|
|
28
|
+
lucky_cli-0.0.3.dist-info/METADATA,sha256=IyC1y2DK5fxsPXpC65qHNmLVdeVk-d9VMC9fSa181n8,5307
|
|
29
|
+
lucky_cli-0.0.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
30
|
+
lucky_cli-0.0.3.dist-info/entry_points.txt,sha256=81gNUKt-U-x2nbT0AU9kScWulDzMWGhbzzCNhwF_iIg,40
|
|
31
|
+
lucky_cli-0.0.3.dist-info/scm_file_list.json,sha256=nz6yoBqqevDlHTJb6nn3SZeglniyNnYSgi4v9Y_GW3M,1743
|
|
32
|
+
lucky_cli-0.0.3.dist-info/scm_version.json,sha256=x9sIn6TrwxSWq1vxhGPRLE3e9qrll_LI-Kg5ZaQ2ujA,160
|
|
33
|
+
lucky_cli-0.0.3.dist-info/top_level.txt,sha256=qC4BUwFImNyKkiZlGP2y4HecuUv4DppyIXN7wTt1TlI,6
|
|
34
|
+
lucky_cli-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PyDeployer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
"README.md",
|
|
4
|
+
"uv.lock",
|
|
5
|
+
".python-version",
|
|
6
|
+
"LICENSE",
|
|
7
|
+
"pyproject.toml",
|
|
8
|
+
"AGENTS.md",
|
|
9
|
+
"CLAUDE.md",
|
|
10
|
+
".gitignore",
|
|
11
|
+
"docs/superpowers/specs/2026-06-21-lucky-cli-design.md",
|
|
12
|
+
"docs/superpowers/plans/2026-06-21-lucky-cli-v1.md",
|
|
13
|
+
"src/lucky/__init__.py",
|
|
14
|
+
"src/lucky/config.py",
|
|
15
|
+
"src/lucky/cli.py",
|
|
16
|
+
"src/lucky/commands/__init__.py",
|
|
17
|
+
"src/lucky/commands/pick.py",
|
|
18
|
+
"src/lucky/commands/show.py",
|
|
19
|
+
"src/lucky/commands/check.py",
|
|
20
|
+
"src/lucky/updater/updater.py",
|
|
21
|
+
"src/lucky/updater/__init__.py",
|
|
22
|
+
"src/lucky/updater/sources.py",
|
|
23
|
+
"src/lucky/updater/client.py",
|
|
24
|
+
"src/lucky/updater/fixtures/powerball.json",
|
|
25
|
+
"src/lucky/updater/fixtures/mega-millions.json",
|
|
26
|
+
"src/lucky/console/__init__.py",
|
|
27
|
+
"src/lucky/console/rich.py",
|
|
28
|
+
"src/lucky/lottery/mega.py",
|
|
29
|
+
"src/lucky/lottery/generator.py",
|
|
30
|
+
"src/lucky/lottery/powerball.py",
|
|
31
|
+
"src/lucky/lottery/__init__.py",
|
|
32
|
+
"src/lucky/lottery/stats.py",
|
|
33
|
+
"src/lucky/lottery/checker.py",
|
|
34
|
+
"src/lucky/lottery/base.py",
|
|
35
|
+
"src/lucky/database/__init__.py",
|
|
36
|
+
"src/lucky/database/repository.py",
|
|
37
|
+
"src/lucky/database/models.py",
|
|
38
|
+
"src/lucky/database/db.py",
|
|
39
|
+
"tests/test_command_pick.py",
|
|
40
|
+
"tests/test_models.py",
|
|
41
|
+
"tests/test_repository.py",
|
|
42
|
+
"tests/test_command_check.py",
|
|
43
|
+
"tests/test_client.py",
|
|
44
|
+
"tests/test_console.py",
|
|
45
|
+
"tests/test_command_show.py",
|
|
46
|
+
"tests/test_stats.py",
|
|
47
|
+
"tests/conftest.py",
|
|
48
|
+
"tests/test_base.py",
|
|
49
|
+
"tests/test_updater.py",
|
|
50
|
+
"tests/test_games.py",
|
|
51
|
+
"tests/test_sources.py",
|
|
52
|
+
"tests/test_generator.py",
|
|
53
|
+
"tests/test_cli.py",
|
|
54
|
+
"tests/test_checker.py",
|
|
55
|
+
"tests/test_db.py",
|
|
56
|
+
".github/workflows/release.yml"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lucky
|