Coverage for an_website/commitment/commitment.py: 96.226%
53 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-01 07:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-01 07:01 +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"""
15Get cool commit messages.
17Based on: https://github.com/ngerakines/commitment
18"""
20import logging
21import random
22from collections.abc import Mapping
23from dataclasses import dataclass
24from datetime import UTC, datetime
25from typing import Final
27from tornado.web import HTTPError
28from typed_stream import Stream
30from an_website.utils.emoji import text_contains_emoji
32from .. import DIR as ROOT_DIR
33from ..utils.data_parsing import parse_args
34from ..utils.request_handler import APIRequestHandler
35from ..utils.utils import ModuleInfo
37LOGGER: Final = logging.getLogger(__name__)
39type Commit = tuple[datetime, str]
40type Commits = Mapping[str, Commit]
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 )
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 }
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"))
74COMMITS: None | Commits = read_commits_txt()
77@dataclass(slots=True)
78class Arguments:
79 """The arguments for the commitment API."""
81 hash: str | None = None
82 require_emoji: bool = False
85class CommitmentAPI(APIRequestHandler):
86 """The request handler for the commitment API."""
88 POSSIBLE_CONTENT_TYPES = (
89 "text/plain",
90 *APIRequestHandler.POSSIBLE_CONTENT_TYPES,
91 )
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 )
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 text_contains_emoji(msg)
110 ]
111 )
112 )
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)
119 if len(args.hash) + 1 >= 42:
120 raise HTTPError(404)
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 text_contains_emoji(msg)
128 )
129 .limit(2)
130 .collect()
131 )
133 if len(results) != 1:
134 raise HTTPError(404)
136 [(hash_, commit)] = results
138 return await self.write_commit(hash_, commit)
140 async def write_commit(self, hash_: str, commit: Commit) -> None:
141 """Write the commit data."""
142 self.set_header("X-Commit-Hash", hash_)
144 if self.content_type == "text/plain":
145 return await self.finish(commit[1])
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 )