Coverage for an_website / hangman_solver / hangman_solver.py: 78.571%
56 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 17:35 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 17:35 +0000
1# This program is free software: you can redistribute it and/or modify
2# it under the terms of the GNU Affero General Public License as
3# published by the Free Software Foundation, either version 3 of the
4# License, or (at your option) any later version.
5#
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU Affero General Public License for more details.
10#
11# You should have received a copy of the GNU Affero General Public License
12# along with this program. If not, see <https://www.gnu.org/licenses/>.
14"""A page that helps solving hangman puzzles."""
16from dataclasses import dataclass
18from hangman_solver import (
19 HangmanResult,
20 Language,
21 UnknownLanguageError,
22 read_words_with_length,
23 solve,
24 solve_crossword,
25)
26from tornado.web import HTTPError
28from ..utils.base_request_handler import BaseRequestHandler
29from ..utils.data_parsing import parse_args
30from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
31from ..utils.utils import ModuleInfo
34def get_module_info() -> ModuleInfo:
35 """Create and return the ModuleInfo for this module."""
36 languages = "|".join([lang.value.lower() for lang in Language.values()])
37 return ModuleInfo(
38 handlers=(
39 (r"/hangman-loeser", HangmanSolver),
40 (r"/api/hangman-loeser", HangmanSolverAPI),
41 (
42 rf"/hangman-loeser/worte/({languages})/([1-9]\d*).txt",
43 HangmanSolverWords,
44 ),
45 ),
46 name="Hangman-Löser",
47 description="Eine Webseite, die Lösungen für Galgenmännchen findet",
48 path="/hangman-loeser",
49 keywords=("Galgenmännchen", "Hangman", "Löser", "Solver", "Worte"),
50 aliases=(
51 "/hangman-l%C3%B6ser",
52 "/hangman-löser",
53 "/hangman-solver",
54 ),
55 )
58@dataclass(slots=True)
59class HangmanArguments:
60 """Arguments for the hangman solver."""
62 max_words: int = 20
63 crossword_mode: bool = False
64 lang: str = "de"
65 input: str = ""
66 invalid: str = ""
68 def get_max_words(self) -> int:
69 """Return the maximum number of words."""
70 return max(0, min(100, self.max_words))
73def solve_hangman(data: HangmanArguments) -> HangmanResult:
74 """Generate a hangman object based on the input and return it."""
75 try:
76 lang = Language.parse_string(data.lang)
77 except UnknownLanguageError as err:
78 raise HTTPError(
79 400, reason=f"{data.lang!r} is an invalid language"
80 ) from err
82 return (solve_crossword if data.crossword_mode else solve)(
83 data.input, data.invalid, lang, data.get_max_words()
84 )
87class HangmanSolver(HTMLRequestHandler):
88 """Request handler for the hangman solver page."""
90 RATELIMIT_GET_LIMIT = 10
92 @parse_args(type_=HangmanArguments, name="data")
93 async def get(self, *, data: HangmanArguments, head: bool = False) -> None:
94 """Handle GET requests to the hangman solver page."""
95 if head:
96 return
98 await self.render(
99 "pages/hangman_solver.html",
100 hangman_result=solve_hangman(data),
101 data=data,
102 )
105class HangmanSolverAPI(APIRequestHandler, HangmanSolver):
106 """Request handler for the hangman solver API."""
108 RATELIMIT_GET_LIMIT = 10
110 @parse_args(type_=HangmanArguments, name="data")
111 async def get(self, *, data: HangmanArguments, head: bool = False) -> None:
112 """Handle GET requests to the hangman solver API."""
113 if head:
114 return
115 hangman_result = solve_hangman(data)
116 await self.finish(
117 {
118 "input": hangman_result.input,
119 "invalid": "".join(hangman_result.invalid),
120 "words": hangman_result.words,
121 "word_count": hangman_result.matching_words_count,
122 "letters": dict(hangman_result.letter_frequency),
123 "crossword_mode": data.crossword_mode,
124 "max_words": data.get_max_words(),
125 "lang": hangman_result.language.value,
126 }
127 )
130class HangmanSolverWords(BaseRequestHandler):
131 """Request handler for the hangman word lists."""
133 RATELIMIT_GET_LIMIT = 15
134 POSSIBLE_CONTENT_TYPES = ("text/plain",)
136 async def get(
137 self, language: str, length: str, *, head: bool = False
138 ) -> None:
139 """Handle GET requests to the hangman solver page."""
140 # pylint: disable=unused-argument
141 try:
142 lang = Language.parse_string(language)
143 except UnknownLanguageError as err:
144 raise HTTPError(404) from err
146 try:
147 word_length = int(length)
148 except ValueError as err:
149 raise HTTPError(404) from err
151 for word in read_words_with_length(lang, word_length):
152 self.write(word)
153 self.write(b"\n")
155 await self.finish()