quizzy 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
quizzy/app.py CHANGED
@@ -189,7 +189,7 @@ class QuestionButton(widgets.Button):
189
189
  def wait_for_result(team: QuestionScreenResult) -> None:
190
190
  if team is None:
191
191
  return
192
- # First, disable the button to prevent multiple clicks
192
+
193
193
  self.disabled = True
194
194
  self.question.answered = True
195
195
  if isinstance(team, NoCorrectAnswerType):
@@ -244,8 +244,17 @@ class QuizzyApp(app.App[None]):
244
244
 
245
245
 
246
246
  def get_arg_parser() -> argparse.ArgumentParser:
247
- parser = argparse.ArgumentParser(prog=__name__.split(".")[0], description="A terminal quiz app")
247
+ parser = argparse.ArgumentParser(
248
+ prog=__name__.split(".")[0],
249
+ description="A quiz app to run in the terminal or the browser",
250
+ epilog=(
251
+ "When an unfinished quiz is closed, the state is saved to a timestamped quiz file in the current working "
252
+ "directory."
253
+ ),
254
+ )
255
+
248
256
  parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
257
+ parser.add_argument("--dump-state-on-exit", action="store_true", help="Dump game state on exit")
249
258
 
250
259
  serve_group = parser.add_argument_group("Serve options")
251
260
  serve_group.add_argument("--serve", action="store_true", help="Serve the app through the browser")
@@ -256,18 +265,43 @@ def get_arg_parser() -> argparse.ArgumentParser:
256
265
  return parser
257
266
 
258
267
 
268
+ def serve_app(host: str, port: int) -> None:
269
+ """Serve the app through textual-serve.
270
+
271
+ :param host: Host to serve the app on
272
+ :param port: Port to serve the app on
273
+ """
274
+ # The --serve flag is set, drop it from the args and serve the app instead through textual-serve
275
+ args = list(sys.argv)
276
+ args.remove("--serve")
277
+
278
+ server.Server(shlex.join(args), host=host, port=port).serve()
279
+
280
+
281
+ def run_app(quiz_file: pathlib.Path, dump_state_on_exit: bool = False) -> None:
282
+ """Run the app in the terminal.
283
+
284
+ :param quizfile: File to use for this quiz
285
+ """
286
+ try:
287
+ config = models.load_config(quiz_file)
288
+ except Exception as e:
289
+ print(f"Error loading quiz file: {e}", file=sys.stderr)
290
+ sys.exit(1)
291
+ app = QuizzyApp(config)
292
+ try:
293
+ app.run()
294
+ finally:
295
+ if dump_state_on_exit:
296
+ models.dump_config_if_not_finished(config)
297
+
298
+
259
299
  def main() -> NoReturn:
260
300
  parser = get_arg_parser()
261
301
  namespace = parser.parse_args()
262
302
 
263
303
  if namespace.serve:
264
- # The --serve flag is set, drop it from the args and serve the app instead through textual-serve
265
- args = list(sys.argv)
266
- args.remove("--serve")
267
-
268
- server.Server(shlex.join(args), host=namespace.host, port=namespace.port).serve()
304
+ serve_app(namespace.host, namespace.port)
269
305
  else:
270
- config = models.load_config(namespace.quizfile)
271
- app = QuizzyApp(config)
272
- app.run()
306
+ run_app(namespace.quizfile, namespace.dump_state_on_exit)
273
307
  sys.exit(0)
quizzy/models.py CHANGED
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime
4
+ import json
3
5
  import pathlib
4
- from typing import Self
6
+ from typing import Annotated, Any, Self
5
7
 
8
+ import pydantic
6
9
  import yaml
7
10
  from pydantic import Field, model_validator
8
11
  from pydantic.dataclasses import dataclass
@@ -21,12 +24,20 @@ class Team:
21
24
  return self.name
22
25
 
23
26
 
27
+ def is_multiple_of_100(v: int) -> int:
28
+ if v % 100 != 0:
29
+ raise ValueError("Value must be a multiple of 100")
30
+ return v
31
+
32
+
24
33
  @dataclass
25
34
  class Question:
26
- question: str
27
- answer: str
28
- value: int = Field(gt=0)
29
- answered: bool = False
35
+ question: str = Field(description="The question to ask. Markdown is supported.")
36
+ answer: str = Field(description="The answer to the question. Markdown is supported.")
37
+ value: Annotated[int, pydantic.AfterValidator(is_multiple_of_100)] = Field(
38
+ gt=0, description="The value of the question in points. Must be a multiple of 100."
39
+ )
40
+ answered: bool = Field(default=False, description="Whether the question has been answered already.")
30
41
 
31
42
  def __str__(self) -> str:
32
43
  return self.question
@@ -34,8 +45,8 @@ class Question:
34
45
 
35
46
  @dataclass
36
47
  class Category:
37
- name: str
38
- questions: list[Question] = Field(max_length=5)
48
+ name: str = Field(description="The name of the category.")
49
+ questions: list[Question] = Field(max_length=5, description="The questions in this category.")
39
50
 
40
51
  @model_validator(mode="after")
41
52
  def sort_questions(self) -> Self:
@@ -47,13 +58,46 @@ class Category:
47
58
 
48
59
  @dataclass
49
60
  class Config:
50
- categories: list[Category] = Field(max_length=5)
51
- teams: list[Team] = Field(default=[Team("Team 1"), Team("Team 2")])
52
- # TODO: Support random questions
61
+ categories: list[Category] = Field(max_length=5, description="The categories in the quiz.")
62
+ teams: list[Team] = Field(default=[Team("Team 1"), Team("Team 2")], description="The teams in the quiz.")
53
63
 
64
+ def is_finished(self) -> bool:
65
+ for category in self.categories:
66
+ for question in category.questions:
67
+ if not question.answered:
68
+ return False
69
+ return True
54
70
 
55
- def load_config(path: pathlib.Path) -> Config:
71
+
72
+ def _get_dict(path: pathlib.Path) -> dict[str, Any]:
73
+ """Get the dictionary from a file.
74
+
75
+ :param path: Path of the file to load.
76
+ :raises ValueError: Raised if the file format is not supported.
77
+ :return: Dictionary with the contents of the file.
78
+ """
56
79
  with path.open() as f:
57
- raw = yaml.safe_load(f)
80
+ if path.suffix == ".json":
81
+ return json.load(f)
82
+ if path.suffix in {".yml", ".yaml"}:
83
+ return yaml.safe_load(f)
84
+ raise ValueError(f"Unsupported file format: {path.suffix}")
58
85
 
86
+
87
+ def load_config(path: pathlib.Path) -> Config:
88
+ raw = _get_dict(path)
59
89
  return Config(**raw)
90
+
91
+
92
+ def dump_config_if_not_finished(config: Config) -> None:
93
+ """Dump the config if any of the questions is not answered yet.
94
+
95
+ :param config: _description_
96
+ """
97
+ if config.is_finished():
98
+ return
99
+ p = pathlib.Path() / f"quizzy-run-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}.json"
100
+ print(f"This quiz is not finished yet. Saving current state to '{p}'.")
101
+ print("This file can be re-used later.")
102
+ with p.open("wb") as f:
103
+ f.write(pydantic.TypeAdapter(Config).dump_json(config))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quizzy
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: A Python TUI quiz app
5
5
  Author-email: Jonas Ehrlich <jonas.ehrlich@gmail.com>
6
6
  License-Expression: MIT
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  A quiz app using [textual](https://textual.textualize.io/).
18
18
 
19
- ![Question board](assets/question-board.png)
19
+ ![Question board](assets/game.gif)
20
20
 
21
21
  ## Configuration and Questions
22
22
 
@@ -0,0 +1,10 @@
1
+ quizzy/__init__.py,sha256=IILIKzO2OO60Rhsh3SqhdKHDf7XI_RLRwzztBGZZmHY,78
2
+ quizzy/__main__.py,sha256=GiZpRpqbaOVG0pDtJswF_VsLByVFP2VMhHgm0Lm0LWw,36
3
+ quizzy/app.py,sha256=gO5aGchsu9PLjepdrgcaglHbo2Wc_1B-AFOszpjU--A,11086
4
+ quizzy/models.py,sha256=CxWOvAi5TbZZK76-aKRIPZhrDbHNM4U3tcrZaQ7xCJs,3189
5
+ quizzy/quizzy.tcss,sha256=VcPEGQ1J7oGatPf3kB-Hzo9AHQdrI7cOV8hDT_l9-3A,1810
6
+ quizzy-0.6.0.dist-info/METADATA,sha256=ffwK69fcadxDA5KDfIcdO_OMfKedgPT9y6s6jZ5ASVU,1598
7
+ quizzy-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ quizzy-0.6.0.dist-info/entry_points.txt,sha256=2RiVMgcS4h7TM59u9dyBQFm53cG6Eyekmb8fqZ5rXHM,48
9
+ quizzy-0.6.0.dist-info/licenses/LICENSE,sha256=JWN3MACgsucm6y_rlL_2MUzst0-wNh-Wab3XkxtfVzM,1070
10
+ quizzy-0.6.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- quizzy/__init__.py,sha256=IILIKzO2OO60Rhsh3SqhdKHDf7XI_RLRwzztBGZZmHY,78
2
- quizzy/__main__.py,sha256=GiZpRpqbaOVG0pDtJswF_VsLByVFP2VMhHgm0Lm0LWw,36
3
- quizzy/app.py,sha256=K1f0_wzcoAsTHbu4VPxKqIdb-j5rrLawWkR1Ez9PD-s,10173
4
- quizzy/models.py,sha256=JrAc2IvtXByIKNg_lX7uQMK19zwi2WsYcNbttXccbOs,1265
5
- quizzy/quizzy.tcss,sha256=VcPEGQ1J7oGatPf3kB-Hzo9AHQdrI7cOV8hDT_l9-3A,1810
6
- quizzy-0.4.0.dist-info/METADATA,sha256=lk2-Z9mhWOUrVla4dbrC5VmFshu4Z8KXIMmbjhYww44,1608
7
- quizzy-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- quizzy-0.4.0.dist-info/entry_points.txt,sha256=2RiVMgcS4h7TM59u9dyBQFm53cG6Eyekmb8fqZ5rXHM,48
9
- quizzy-0.4.0.dist-info/licenses/LICENSE,sha256=JWN3MACgsucm6y_rlL_2MUzst0-wNh-Wab3XkxtfVzM,1070
10
- quizzy-0.4.0.dist-info/RECORD,,
File without changes