quizzy 0.1.0__tar.gz → 0.3.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {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
|