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

54 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-01 08:32 +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 

20from __future__ import annotations 

21 

22import logging 

23import random 

24from collections.abc import Mapping 

25from dataclasses import dataclass 

26from datetime import UTC, datetime 

27from typing import Final 

28 

29import emoji 

30from tornado.web import HTTPError 

31from typed_stream import Stream 

32 

33from .. import DIR as ROOT_DIR 

34from ..utils.data_parsing import parse_args 

35from ..utils.request_handler import APIRequestHandler 

36from ..utils.utils import ModuleInfo 

37 

38LOGGER: Final = logging.getLogger(__name__) 

39 

40type Commit = tuple[datetime, str] 

41type Commits = Mapping[str, Commit] 

42 

43 

44def get_module_info() -> ModuleInfo: 

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

46 return ModuleInfo( 

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

48 name="Commitment", 

49 short_name="Commitment", 

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

51 path="/api/commitment", 

52 aliases=(), 

53 sub_pages=(), 

54 keywords=(), 

55 hidden=True, 

56 ) 

57 

58 

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

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

61 return { 

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

63 for line in data.splitlines() 

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

65 } 

66 

67 

68def read_commits_txt() -> None | Commits: 

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

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

71 return None 

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

73 

74 

75COMMITS: None | Commits = read_commits_txt() 

76 

77 

78@dataclass(slots=True) 

79class Arguments: 

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

81 

82 hash: str | None = None 

83 require_emoji: bool = False 

84 

85 

86class CommitmentAPI(APIRequestHandler): 

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

88 

89 POSSIBLE_CONTENT_TYPES = ( 

90 "text/plain", 

91 *APIRequestHandler.POSSIBLE_CONTENT_TYPES, 

92 ) 

93 

94 @parse_args(type_=Arguments) 

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

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

97 # pylint: disable=unused-argument 

98 if not COMMITS: 

99 raise HTTPError( 

100 503, 

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

102 ) 

103 

104 if args.hash is None: 

105 return await self.write_commit( 

106 *random.choice( 

107 [ 

108 (com, (_, msg)) 

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

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

111 ] 

112 ) 

113 ) 

114 

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

116 if args.hash in COMMITS: 

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

118 raise HTTPError(404) 

119 

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

121 raise HTTPError(404) 

122 

123 results = ( 

124 Stream( 

125 (com, (_, msg)) 

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

127 if com.startswith(args.hash) 

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

129 ) 

130 .limit(2) 

131 .collect() 

132 ) 

133 

134 if len(results) != 1: 

135 raise HTTPError(404) 

136 

137 [(hash_, commit)] = results 

138 

139 return await self.write_commit(hash_, commit) 

140 

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

142 """Write the commit data.""" 

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

144 

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

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

147 

148 return await self.finish_dict( 

149 hash=hash_, 

150 commit_message=commit[1], 

151 permalink=self.fix_url("/api/commitment", hash=hash_), 

152 date=commit[0], 

153 )