quizzy 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quizzy-0.1.0 → quizzy-0.3.0}/.pre-commit-config.yaml +3 -5
- quizzy-0.3.0/CHANGELOG.md +14 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/PKG-INFO +9 -5
- {quizzy-0.1.0 → quizzy-0.3.0}/README.md +8 -4
- quizzy-0.3.0/examples/quizzy.yaml +24 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/pyproject.toml +1 -1
- {quizzy-0.1.0 → quizzy-0.3.0}/quizzy/app.py +60 -18
- {quizzy-0.1.0 → quizzy-0.3.0}/quizzy/quizzy.tcss +25 -5
- {quizzy-0.1.0 → quizzy-0.3.0}/uv.lock +1 -1
- quizzy-0.1.0/examples/quizzy.yaml +0 -21
- {quizzy-0.1.0 → quizzy-0.3.0}/.github/workflows/ci.yaml +0 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/.github/workflows/publish-to-pypi.yaml +0 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/.gitignore +0 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/LICENSE +0 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/assets/question-board.png +0 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/quizzy/__init__.py +0 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/quizzy/__main__.py +0 -0
- {quizzy-0.1.0 → quizzy-0.3.0}/quizzy/models.py +0 -0
@@ -5,15 +5,13 @@ repos:
|
|
5
5
|
- id: check-yaml
|
6
6
|
- id: end-of-file-fixer
|
7
7
|
- id: trailing-whitespace
|
8
|
-
- repo: https://github.com/psf/black
|
9
|
-
rev: "24.10.0"
|
10
|
-
hooks:
|
11
|
-
- id: black
|
12
8
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
13
9
|
# Ruff version.
|
14
10
|
rev: "v0.8.3"
|
15
11
|
hooks:
|
16
|
-
|
12
|
+
- id: ruff
|
13
|
+
args: [ --fix ]
|
14
|
+
- id: ruff-format
|
17
15
|
- repo: https://github.com/pycqa/isort
|
18
16
|
rev: "5.13.2"
|
19
17
|
hooks:
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## v0.3.0
|
4
|
+
|
5
|
+
* Add *Escape* keybinding to dismiss question and answer modals
|
6
|
+
* Add score modifier buttons
|
7
|
+
|
8
|
+
## v0.2.0
|
9
|
+
|
10
|
+
* Added support for Markdown syntax in questions and answers
|
11
|
+
|
12
|
+
## v0.1.0
|
13
|
+
|
14
|
+
* Initial release with basic functionality
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: quizzy
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: A Python TUI quiz app
|
5
5
|
Author-email: Jonas Ehrlich <jonas.ehrlich@gmail.com>
|
6
6
|
License-Expression: MIT
|
@@ -17,7 +17,6 @@ A quiz app using [textual](https://textual.textualize.io/).
|
|
17
17
|
|
18
18
|

|
19
19
|
|
20
|
-
|
21
20
|
## Configuration and Questions
|
22
21
|
|
23
22
|
Create a YAML file to define the teams participating, the questions and their answers.
|
@@ -47,16 +46,21 @@ categories:
|
|
47
46
|
|
48
47
|
See [examples/quizzy.yaml](examples/quizzy.yaml) for an example.
|
49
48
|
|
50
|
-
|
51
49
|
## Running it
|
52
50
|
|
53
|
-
Run
|
51
|
+
Run the latest PyPI release using *uvx*:
|
52
|
+
|
53
|
+
```sh
|
54
|
+
uvx quizzy projects/gh/quizzy/examples/quizzy.yaml
|
55
|
+
```
|
56
|
+
|
57
|
+
Run the local version using *uv*:
|
54
58
|
|
55
59
|
``` sh
|
56
60
|
uv run quizzy examples/quizzy.yaml
|
57
61
|
```
|
58
62
|
|
59
|
-
Serve using textual:
|
63
|
+
Serve on a webserver using textual:
|
60
64
|
|
61
65
|
``` sh
|
62
66
|
uv run textual serve "uv run quizzy examples/quizzy.yaml"
|
@@ -4,7 +4,6 @@ A quiz app using [textual](https://textual.textualize.io/).
|
|
4
4
|
|
5
5
|

|
6
6
|
|
7
|
-
|
8
7
|
## Configuration and Questions
|
9
8
|
|
10
9
|
Create a YAML file to define the teams participating, the questions and their answers.
|
@@ -34,16 +33,21 @@ categories:
|
|
34
33
|
|
35
34
|
See [examples/quizzy.yaml](examples/quizzy.yaml) for an example.
|
36
35
|
|
37
|
-
|
38
36
|
## Running it
|
39
37
|
|
40
|
-
Run
|
38
|
+
Run the latest PyPI release using *uvx*:
|
39
|
+
|
40
|
+
```sh
|
41
|
+
uvx quizzy projects/gh/quizzy/examples/quizzy.yaml
|
42
|
+
```
|
43
|
+
|
44
|
+
Run the local version using *uv*:
|
41
45
|
|
42
46
|
``` sh
|
43
47
|
uv run quizzy examples/quizzy.yaml
|
44
48
|
```
|
45
49
|
|
46
|
-
Serve using textual:
|
50
|
+
Serve on a webserver using textual:
|
47
51
|
|
48
52
|
``` sh
|
49
53
|
uv run textual serve "uv run quizzy examples/quizzy.yaml"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
teams:
|
2
|
+
- name: "Team 1"
|
3
|
+
- name: "Team 2"
|
4
|
+
categories:
|
5
|
+
- name: "General Knowledge"
|
6
|
+
questions:
|
7
|
+
- question: "What is the capital of France?"
|
8
|
+
answer: "Paris"
|
9
|
+
value: 100
|
10
|
+
- question: "What is the capital of Germany?"
|
11
|
+
answer: "Berlin"
|
12
|
+
value: 200
|
13
|
+
- name: "Science"
|
14
|
+
questions:
|
15
|
+
- question: "What is the _chemical_ **symbol** for [gold](https://duckduckgo.com/?t=ffab&q=gold)?"
|
16
|
+
answer: "Au"
|
17
|
+
value: 100
|
18
|
+
- question: |
|
19
|
+
What is the chemical symbol for silver?
|
20
|
+
|
21
|
+
- We really need to know this one. And don't care about platinum which is Pt.
|
22
|
+
- Also not Helium which is He.
|
23
|
+
answer: "[Ag](https://en.wikipedia.org/wiki/Gold), that was easy **right**?"
|
24
|
+
value: 200
|
@@ -3,10 +3,13 @@ from __future__ import annotations
|
|
3
3
|
import argparse
|
4
4
|
import pathlib
|
5
5
|
|
6
|
-
from textual import app, containers, log, message, reactive, screen, widgets
|
6
|
+
from textual import app, binding, containers, log, message, reactive, screen, widgets
|
7
7
|
|
8
8
|
from quizzy import __version__, models
|
9
9
|
|
10
|
+
NoCorrectAnswerType = type("NoCorrectAnswerType", (object,), {})
|
11
|
+
NoCorrectAnswer = NoCorrectAnswerType()
|
12
|
+
|
10
13
|
|
11
14
|
def get_arg_parser() -> argparse.ArgumentParser:
|
12
15
|
parser = argparse.ArgumentParser(prog=__name__.split(".")[0], description="A terminal quiz app")
|
@@ -15,9 +18,16 @@ def get_arg_parser() -> argparse.ArgumentParser:
|
|
15
18
|
return parser
|
16
19
|
|
17
20
|
|
18
|
-
|
21
|
+
QuestionScreenResult = models.Team | NoCorrectAnswerType | None
|
22
|
+
|
23
|
+
|
24
|
+
class AnswerScreen(screen.ModalScreen[QuestionScreenResult], can_focus=True):
|
19
25
|
NOONE_ANSWERED_ID = "__noone-answered"
|
20
26
|
|
27
|
+
BINDINGS = [
|
28
|
+
binding.Binding("escape", "no_correct_answer", "Dismiss", key_display="Esc"),
|
29
|
+
]
|
30
|
+
|
21
31
|
def __init__(self, category: str, question: models.Question, teams: list[models.Team]) -> None:
|
22
32
|
super().__init__(classes="question-answer-screen")
|
23
33
|
self.category = category
|
@@ -26,13 +36,13 @@ class AnswerScreen(screen.ModalScreen[models.Team | None]):
|
|
26
36
|
self.border_title = f"{category} - {question.value} points"
|
27
37
|
|
28
38
|
def compose(self) -> app.ComposeResult:
|
29
|
-
question_widget = widgets.
|
39
|
+
question_widget = widgets.Markdown(self.question.question, id="question")
|
30
40
|
question_widget.border_title = "Question"
|
31
41
|
|
32
|
-
answer_widget = widgets.
|
42
|
+
answer_widget = widgets.Markdown(self.question.answer, id="answer")
|
33
43
|
answer_widget.border_title = "Answer"
|
34
44
|
|
35
|
-
|
45
|
+
who_answered = containers.HorizontalGroup(
|
36
46
|
*[
|
37
47
|
containers.Vertical(widgets.Button(team.name, id=team_id, variant="primary"))
|
38
48
|
for team_id, team in self.teams.items()
|
@@ -40,12 +50,12 @@ class AnswerScreen(screen.ModalScreen[models.Team | None]):
|
|
40
50
|
id="who-answered",
|
41
51
|
classes="horizontal-100",
|
42
52
|
)
|
43
|
-
|
53
|
+
who_answered.border_title = "Who Answered Correctly?"
|
44
54
|
|
45
55
|
container = containers.Grid(
|
46
56
|
question_widget,
|
47
57
|
answer_widget,
|
48
|
-
|
58
|
+
who_answered,
|
49
59
|
containers.Horizontal(
|
50
60
|
widgets.Button(
|
51
61
|
"😭 No one answered correctly 😭", id=self.NOONE_ANSWERED_ID, variant="error", classes="button-100"
|
@@ -57,18 +67,25 @@ class AnswerScreen(screen.ModalScreen[models.Team | None]):
|
|
57
67
|
)
|
58
68
|
|
59
69
|
container.border_title = f"{self.category} - {self.question.value} points"
|
70
|
+
yield widgets.Footer()
|
60
71
|
yield container
|
61
72
|
|
73
|
+
def action_no_correct_answer(self) -> None:
|
74
|
+
self.dismiss(NoCorrectAnswer)
|
75
|
+
|
62
76
|
def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
|
63
77
|
if event.button.id == self.NOONE_ANSWERED_ID:
|
64
|
-
self.dismiss(
|
78
|
+
self.dismiss(NoCorrectAnswer)
|
65
79
|
elif event.button.id in self.teams:
|
66
80
|
team = self.teams[event.button.id]
|
67
81
|
self.dismiss(team)
|
68
82
|
|
69
83
|
|
70
|
-
class QuestionScreen(screen.ModalScreen[
|
84
|
+
class QuestionScreen(screen.ModalScreen[QuestionScreenResult], can_focus=True):
|
71
85
|
SHOW_ANSWER_ID = "show-answer"
|
86
|
+
BINDINGS = [
|
87
|
+
binding.Binding("escape", "dismiss(None)", "Dismiss"),
|
88
|
+
]
|
72
89
|
|
73
90
|
def __init__(self, category: str, question: models.Question, teams: list[models.Team]) -> None:
|
74
91
|
super().__init__(classes="question-answer-screen")
|
@@ -77,7 +94,7 @@ class QuestionScreen(screen.ModalScreen[models.Team | None]):
|
|
77
94
|
self.teams = teams
|
78
95
|
|
79
96
|
def compose(self) -> app.ComposeResult:
|
80
|
-
question_widget = widgets.
|
97
|
+
question_widget = widgets.Markdown(self.question.question, id="question")
|
81
98
|
question_widget.border_title = "Question"
|
82
99
|
|
83
100
|
container = containers.Grid(
|
@@ -91,17 +108,22 @@ class QuestionScreen(screen.ModalScreen[models.Team | None]):
|
|
91
108
|
)
|
92
109
|
|
93
110
|
container.border_title = f"{self.category} - {self.question.value} points"
|
111
|
+
yield widgets.Footer()
|
94
112
|
yield container
|
95
113
|
|
96
114
|
def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
|
97
|
-
def dismiss(team:
|
115
|
+
def dismiss(team: QuestionScreenResult) -> None:
|
98
116
|
self.dismiss(team)
|
99
117
|
|
100
118
|
if event.button.id == self.SHOW_ANSWER_ID:
|
119
|
+
event.stop()
|
101
120
|
self.app.push_screen(AnswerScreen(self.category, self.question, self.teams), dismiss)
|
102
121
|
|
103
122
|
|
104
|
-
class TeamScore(containers.
|
123
|
+
class TeamScore(containers.Horizontal):
|
124
|
+
MODIFIER_BUTTON_VALUE = 100
|
125
|
+
_ADD_BUTTON_ID = f"add-{MODIFIER_BUTTON_VALUE}"
|
126
|
+
_SUBTRACT_BUTTON_ID = f"subtract-{MODIFIER_BUTTON_VALUE}"
|
105
127
|
score = reactive.reactive(0, recompose=True)
|
106
128
|
|
107
129
|
def __init__(self, team: models.Team) -> None:
|
@@ -112,6 +134,22 @@ class TeamScore(containers.Vertical):
|
|
112
134
|
|
113
135
|
def compose(self) -> app.ComposeResult:
|
114
136
|
yield widgets.Static(str(self.score))
|
137
|
+
yield containers.Horizontal(
|
138
|
+
widgets.Button("+ 100", id=self._ADD_BUTTON_ID, variant="success"),
|
139
|
+
widgets.Button("- 100", id=self._SUBTRACT_BUTTON_ID, variant="error"),
|
140
|
+
classes="modifier-buttons-container",
|
141
|
+
)
|
142
|
+
|
143
|
+
def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
|
144
|
+
if event.button.id == self._ADD_BUTTON_ID:
|
145
|
+
self.score += self.MODIFIER_BUTTON_VALUE
|
146
|
+
event.stop()
|
147
|
+
elif event.button.id == self._SUBTRACT_BUTTON_ID:
|
148
|
+
if self.score <= self.MODIFIER_BUTTON_VALUE:
|
149
|
+
self.score = 0
|
150
|
+
else:
|
151
|
+
self.score -= self.MODIFIER_BUTTON_VALUE
|
152
|
+
event.stop()
|
115
153
|
|
116
154
|
def watch_score(self, score: int) -> None:
|
117
155
|
"""
|
@@ -152,13 +190,14 @@ class QuestionButton(widgets.Button):
|
|
152
190
|
self.disabled = question.answered
|
153
191
|
|
154
192
|
def on_click(self) -> None:
|
155
|
-
|
156
|
-
self.disabled = True
|
157
|
-
self.question.answered = True
|
158
|
-
|
159
|
-
def wait_for_result(team: models.Team | None) -> None:
|
193
|
+
def wait_for_result(team: QuestionScreenResult) -> None:
|
160
194
|
if team is None:
|
161
|
-
|
195
|
+
return
|
196
|
+
# First, disable the button to prevent multiple clicks
|
197
|
+
self.disabled = True
|
198
|
+
self.question.answered = True
|
199
|
+
if isinstance(team, NoCorrectAnswerType):
|
200
|
+
log("question-button: No one answered the question")
|
162
201
|
else:
|
163
202
|
log(f"question-button: {team.id} answered the question")
|
164
203
|
self.post_message(self.Answered(team, self.question.value))
|
@@ -206,3 +245,6 @@ class QuizzyApp(app.App[None]):
|
|
206
245
|
|
207
246
|
def on_question_button_answered(self, event: QuestionButton.Answered) -> None:
|
208
247
|
self.scoreboard_widget.update_team_score(event.team.id, event.value)
|
248
|
+
|
249
|
+
def on_mount(self) -> None:
|
250
|
+
self.theme = "textual-light"
|
@@ -1,8 +1,28 @@
|
|
1
1
|
TeamScore {
|
2
|
-
border:
|
2
|
+
border: round $primary;
|
3
3
|
align: center top;
|
4
4
|
height: 3;
|
5
5
|
margin: 1;
|
6
|
+
|
7
|
+
Static {
|
8
|
+
max-width: 50%;
|
9
|
+
}
|
10
|
+
|
11
|
+
Horizontal.modifier-buttons-container {
|
12
|
+
align: right middle;
|
13
|
+
|
14
|
+
Button {
|
15
|
+
margin: 0 2;
|
16
|
+
padding: 0;
|
17
|
+
min-width: 8;
|
18
|
+
border: none;
|
19
|
+
width: 8;
|
20
|
+
&:focus {
|
21
|
+
/* Disable reversing for text of focussed buttons in the team score row */
|
22
|
+
text-style: bold;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
6
26
|
}
|
7
27
|
|
8
28
|
|
@@ -15,20 +35,20 @@ TeamScore {
|
|
15
35
|
height: 80%;
|
16
36
|
min-height: 40;
|
17
37
|
padding: 0 2;
|
18
|
-
border:
|
38
|
+
border: round $primary;
|
19
39
|
background: $surface;
|
20
40
|
content-align: center middle;
|
21
41
|
|
22
42
|
#question {
|
23
43
|
content-align: center middle;
|
24
44
|
padding: 2;
|
25
|
-
border:
|
45
|
+
border: round $foreground 80%;
|
26
46
|
}
|
27
47
|
|
28
48
|
#answer {
|
29
49
|
content-align: center middle;
|
30
50
|
padding: 2;
|
31
|
-
border:
|
51
|
+
border: round $success 80%;
|
32
52
|
|
33
53
|
}
|
34
54
|
}
|
@@ -59,7 +79,7 @@ AnswerScreen {
|
|
59
79
|
grid-rows: 40% 40% 5 4;
|
60
80
|
|
61
81
|
#who-answered {
|
62
|
-
border:
|
82
|
+
border: round $foreground 80%;
|
63
83
|
Button {
|
64
84
|
margin: 0 1;
|
65
85
|
width: 100%;
|
@@ -1,21 +0,0 @@
|
|
1
|
-
teams:
|
2
|
-
- name: "Team 1"
|
3
|
-
- name: "Team 2"
|
4
|
-
categories:
|
5
|
-
- name: "General Knowledge"
|
6
|
-
questions:
|
7
|
-
- question: "What is the capital of France?"
|
8
|
-
answer: "Paris"
|
9
|
-
value: 100
|
10
|
-
- question: "What is the capital of Germany?"
|
11
|
-
answer: "Berlin"
|
12
|
-
value: 200
|
13
|
-
- name: "Science"
|
14
|
-
questions:
|
15
|
-
- question: "What is the chemical symbol for gold?"
|
16
|
-
answer: "Au"
|
17
|
-
value: 100
|
18
|
-
- question: "What is the chemical symbol for silver?"
|
19
|
-
answer: "Ag"
|
20
|
-
value: 200
|
21
|
-
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|