Coverage for an_website / commitment / commitment.py: 96.226%

53 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-15 14:36 +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""" 

15Get cool commit messages. 

16 

17Based on: https://github.com/ngerakines/commitment 

18""" 

19 

20 

21import logging 

22import random 

23from collections.abc import Mapping 

24from dataclasses import dataclass 

25from datetime import UTC, datetime 

26from typing import Final 

27 

28import emoji 

29from tornado.web import HTTPError 

30from typed_stream import Stream 

31 

32from .. import DIR as ROOT_DIR 

33from ..utils.data_parsing import parse_args 

34from ..utils.request_handler import APIRequestHandler 

35from ..utils.utils import ModuleInfo 

36 

37LOGGER: Final = logging.getLogger(__name__) 

38 

39type Commit = tuple[datetime, str] 

40type Commits = Mapping[str, Commit] 

41 

42 

43def get_module_info() -> ModuleInfo: 

44 """Create and return the ModuleInfo for this module.""" 

45 return ModuleInfo( 

46 handlers=((r"/api/commitment", CommitmentAPI),), 

47 name="Commitment", 

48 short_name="Commitment", 

49 description="Zeige gute Commit-Nachrichten an.", 

50 path="/api/commitment", 

51 aliases=(), 

52 sub_pages=(), 

53 keywords=(), 

54 hidden=True, 

55 ) 

56 

57 

58def parse_commits_txt(data: str) -> Commits: 

59 """Parse the contents of commits.txt.""" 

60 return { 

61 split[0]: (datetime.fromtimestamp(int(split[1]), UTC), split[2]) 

62 for line in data.splitlines() 

63 if (split := line.rstrip().split(" ", 2)) 

64 } 

65 

66 

67def read_commits_txt() -> None | Commits: 

68 """Read the contents of the local commits.txt file.""" 

69 if not (file := ROOT_DIR / "static" / "commits.txt").is_file(): 

70 return None 

71 return parse_commits_txt(file.read_text("UTF-8")) 

72 

73 

74COMMITS: None | Commits = read_commits_txt() 

75 

76 

77@dataclass(slots=True) 

78class Arguments: 

79 """The arguments for the commitment API.""" 

80 

81 hash: str | None = None 

82 require_emoji: bool = False 

83 

84 

85class CommitmentAPI(APIRequestHandler): 

86 """The request handler for the commitment API.""" 

87 

88 POSSIBLE_CONTENT_TYPES = ( 

89 "text/plain", 

90 *APIRequestHandler.POSSIBLE_CONTENT_TYPES, 

91 ) 

92 

93 @parse_args(type_=Arguments) 

94 async def get(self, *, args: Arguments, head: bool = False) -> None: 

95 """Handle GET requests to the API.""" 

96 # pylint: disable=unused-argument 

97 if not COMMITS: 

98 raise HTTPError( 

99 503, 

100 log_message="No COMMITS found, make sure to create commits.txt", 

101 ) 

102 

103 if args.hash is None: 

104 return await self.write_commit( 

105 *random.choice( 

106 [ 

107 (com, (_, msg)) 

108 for com, (_, msg) in COMMITS.items() 

109 if not args.require_emoji or any(emoji.analyze(msg)) 

110 ] 

111 ) 

112 ) 

113 

114 if len(args.hash) + 2 == 42: 

115 if args.hash in COMMITS: 

116 return await self.write_commit(args.hash, COMMITS[args.hash]) 

117 raise HTTPError(404) 

118 

119 if len(args.hash) + 1 >= 42: 

120 raise HTTPError(404) 

121 

122 results = ( 

123 Stream( 

124 (com, (_, msg)) 

125 for com, (_, msg) in COMMITS.items() 

126 if com.startswith(args.hash) 

127 if not args.require_emoji or any(emoji.analyze(msg)) 

128 ) 

129 .limit(2) 

130 .collect() 

131 ) 

132 

133 if len(results) != 1: 

134 raise HTTPError(404) 

135 

136 [(hash_, commit)] = results 

137 

138 return await self.write_commit(hash_, commit) 

139 

140 async def write_commit(self, hash_: str, commit: Commit) -> None: 

141 """Write the commit data.""" 

142 self.set_header("X-Commit-Hash", hash_) 

143 

144 if self.content_type == "text/plain": 

145 return await self.finish(commit[1]) 

146 

147 return await self.finish_dict( 

148 hash=hash_, 

149 commit_message=commit[1], 

150 permalink=self.fix_url( 

151 "/api/commitment", query_args={"hash": hash_} 

152 ), 

153 date=commit[0], 

154 )