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

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/>. 

13 

14"""A page that helps solving hangman puzzles.""" 

15 

16from dataclasses import dataclass 

17 

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 

27 

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 

32 

33 

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 ) 

56 

57 

58@dataclass(slots=True) 

59class HangmanArguments: 

60 """Arguments for the hangman solver.""" 

61 

62 max_words: int = 20 

63 crossword_mode: bool = False 

64 lang: str = "de" 

65 input: str = "" 

66 invalid: str = "" 

67 

68 def get_max_words(self) -> int: 

69 """Return the maximum number of words.""" 

70 return max(0, min(100, self.max_words)) 

71 

72 

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 

81 

82 return (solve_crossword if data.crossword_mode else solve)( 

83 data.input, data.invalid, lang, data.get_max_words() 

84 ) 

85 

86 

87class HangmanSolver(HTMLRequestHandler): 

88 """Request handler for the hangman solver page.""" 

89 

90 RATELIMIT_GET_LIMIT = 10 

91 

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 

97 

98 await self.render( 

99 "pages/hangman_solver.html", 

100 hangman_result=solve_hangman(data), 

101 data=data, 

102 ) 

103 

104 

105class HangmanSolverAPI(APIRequestHandler, HangmanSolver): 

106 """Request handler for the hangman solver API.""" 

107 

108 RATELIMIT_GET_LIMIT = 10 

109 

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 ) 

128 

129 

130class HangmanSolverWords(BaseRequestHandler): 

131 """Request handler for the hangman word lists.""" 

132 

133 RATELIMIT_GET_LIMIT = 15 

134 POSSIBLE_CONTENT_TYPES = ("text/plain",) 

135 

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 

145 

146 try: 

147 word_length = int(length) 

148 except ValueError as err: 

149 raise HTTPError(404) from err 

150 

151 for word in read_words_with_length(lang, word_length): 

152 self.write(word) 

153 self.write(b"\n") 

154 

155 await self.finish()