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

53 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-09 14:51 +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 

20import logging 

21import random 

22from collections.abc import Mapping 

23from dataclasses import dataclass 

24from datetime import UTC, datetime 

25from typing import Final 

26 

27from tornado.web import HTTPError 

28from typed_stream import Stream 

29 

30from .. import DIR as ROOT_DIR 

31from ..utils.data_parsing import parse_args 

32from ..utils.emoji import text_contains_emoji 

33from ..utils.request_handler import APIRequestHandler 

34from ..utils.utils import ModuleInfo 

35 

36LOGGER: Final = logging.getLogger(__name__) 

37 

38type Commit = tuple[datetime, str] 

39type Commits = Mapping[str, Commit] 

40 

41 

42def get_module_info() -> ModuleInfo: 

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

44 return ModuleInfo( 

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

46 name="Commitment", 

47 short_name="Commitment", 

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

49 path="/api/commitment", 

50 aliases=(), 

51 sub_pages=(), 

52 keywords=(), 

53 hidden=True, 

54 ) 

55 

56 

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

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

59 return { 

60 split[0]: ( 

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

62 split[2] if len(split) >= 3 else "", 

63 ) 

64 for line in data.splitlines() 

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

66 } 

67 

68 

69def read_commits_txt() -> None | Commits: 

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

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

72 return None 

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

74 

75 

76COMMITS: None | Commits = read_commits_txt() 

77 

78 

79@dataclass(slots=True) 

80class Arguments: 

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

82 

83 hash: str | None = None 

84 require_emoji: bool = False 

85 

86 

87class CommitmentAPI(APIRequestHandler): 

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

89 

90 POSSIBLE_CONTENT_TYPES = ( 

91 "text/plain", 

92 *APIRequestHandler.POSSIBLE_CONTENT_TYPES, 

93 ) 

94 

95 @parse_args(type_=Arguments) 

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

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

98 # pylint: disable=unused-argument 

99 if not COMMITS: 

100 raise HTTPError( 

101 503, 

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

103 ) 

104 

105 if args.hash is None: 

106 return await self.write_commit( 

107 *random.choice( 

108 [ 

109 (com, (_, msg)) 

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

111 if msg 

112 if not args.require_emoji or text_contains_emoji(msg) 

113 ] 

114 ) 

115 ) 

116 

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

118 if args.hash in COMMITS: 

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

120 raise HTTPError(404) 

121 

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

123 raise HTTPError(404) 

124 

125 results = ( 

126 Stream( 

127 (com, (_, msg)) 

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

129 if com.startswith(args.hash) 

130 if not args.require_emoji or text_contains_emoji(msg) 

131 ) 

132 .limit(2) 

133 .collect() 

134 ) 

135 

136 if len(results) != 1: 

137 raise HTTPError(404) 

138 

139 [(hash_, commit)] = results 

140 

141 return await self.write_commit(hash_, commit) 

142 

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

144 """Write the commit data.""" 

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

146 

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

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

149 

150 return await self.finish_dict( 

151 hash=hash_, 

152 commit_message=commit[1], 

153 permalink=self.fix_url( 

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

155 ), 

156 date=commit[0], 

157 )