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
« 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/>.
14"""
15Get cool commit messages.
17Based on: https://github.com/ngerakines/commitment
18"""
20from __future__ import annotations
22import logging
23import random
24from collections.abc import Mapping
25from dataclasses import dataclass
26from datetime import UTC, datetime
27from typing import Final
29import emoji
30from tornado.web import HTTPError
31from typed_stream import Stream
33from .. import DIR as ROOT_DIR
34from ..utils.data_parsing import parse_args
35from ..utils.request_handler import APIRequestHandler
36from ..utils.utils import ModuleInfo
38LOGGER: Final = logging.getLogger(__name__)
40type Commit = tuple[datetime, str]
41type Commits = Mapping[str, Commit]
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 )
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 }
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"))
75COMMITS: None | Commits = read_commits_txt()
78@dataclass(slots=True)
79class Arguments:
80 """The arguments for the commitment API."""
82 hash: str | None = None
83 require_emoji: bool = False
86class CommitmentAPI(APIRequestHandler):
87 """The request handler for the commitment API."""
89 POSSIBLE_CONTENT_TYPES = (
90 "text/plain",
91 *APIRequestHandler.POSSIBLE_CONTENT_TYPES,
92 )
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 )
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 )
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)
120 if len(args.hash) + 1 >= 42:
121 raise HTTPError(404)
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 )
134 if len(results) != 1:
135 raise HTTPError(404)
137 [(hash_, commit)] = results
139 return await self.write_commit(hash_, commit)
141 async def write_commit(self, hash_: str, commit: Commit) -> None:
142 """Write the commit data."""
143 self.set_header("X-Commit-Hash", hash_)
145 if self.content_type == "text/plain":
146 return await self.finish(commit[1])
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 )