kattis-cli 1.0.6__py3-none-any.whl → 1.1.0__py3-none-any.whl
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.
- kattis_cli/client.py +466 -0
- kattis_cli/download.py +196 -145
- kattis_cli/fireworks.py +128 -0
- kattis_cli/kattis.py +68 -412
- kattis_cli/kattis_setup.py +124 -87
- kattis_cli/main.py +3 -3
- kattis_cli/solution_tester.py +221 -0
- kattis_cli/ui.py +112 -71
- {kattis_cli-1.0.6.dist-info → kattis_cli-1.1.0.dist-info}/METADATA +29 -20
- kattis_cli-1.1.0.dist-info/RECORD +21 -0
- {kattis_cli-1.0.6.dist-info → kattis_cli-1.1.0.dist-info}/WHEEL +1 -1
- kattis_cli/test_solution.py +0 -179
- kattis_cli-1.0.6.dist-info/RECORD +0 -19
- {kattis_cli-1.0.6.dist-info → kattis_cli-1.1.0.dist-info}/entry_points.txt +0 -0
- {kattis_cli-1.0.6.dist-info → kattis_cli-1.1.0.dist-info/licenses}/LICENSE +0 -0
kattis_cli/download.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
"""Download sample data and problem metadata from Kattis
|
|
2
|
-
|
|
1
|
+
"""Download sample data and problem metadata from Kattis and save them.
|
|
2
|
+
|
|
3
|
+
This module provides a :class:`DownloadManager` that encapsulates
|
|
4
|
+
the download and metadata parsing logic. For backward compatibility the
|
|
5
|
+
module also exposes the original functions which delegate to a default
|
|
6
|
+
``DownloadManager`` instance.
|
|
3
7
|
"""
|
|
4
8
|
|
|
5
9
|
import shutil
|
|
@@ -12,168 +16,215 @@ from .utils import config, utility
|
|
|
12
16
|
from . import settings
|
|
13
17
|
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
"""
|
|
19
|
+
class DownloadManager:
|
|
20
|
+
"""Manager responsible for fetching problem metadata and sample data."""
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
def create_problem_folder(self, problemid: str) -> Path | Any:
|
|
23
|
+
"""Ensure and return the root folder for a problem.
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
try:
|
|
25
|
-
root_problem_folder = utility.find_problem_root_folder(
|
|
26
|
-
Path.cwd(), f"{problemid}.yaml")
|
|
27
|
-
except FileNotFoundError:
|
|
28
|
-
root_problem_folder = Path.cwd().joinpath(problemid)
|
|
29
|
-
if not root_problem_folder.exists():
|
|
30
|
-
root_problem_folder.mkdir()
|
|
25
|
+
If a problem folder cannot be resolved by searching the current
|
|
26
|
+
working tree, create a new folder named after the problem id.
|
|
31
27
|
|
|
32
|
-
|
|
28
|
+
Args:
|
|
29
|
+
problemid: The Kattis problem id.
|
|
33
30
|
|
|
31
|
+
Returns:
|
|
32
|
+
Path to the root problem folder, or Any on failure.
|
|
33
|
+
"""
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
try:
|
|
36
|
+
root_problem_folder = utility.find_problem_root_folder(
|
|
37
|
+
Path.cwd(), f"{problemid}.yaml")
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
root_problem_folder = Path.cwd().joinpath(problemid)
|
|
40
|
+
if not root_problem_folder.exists():
|
|
41
|
+
root_problem_folder.mkdir()
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
problemid (str): problemid
|
|
40
|
-
"""
|
|
41
|
-
response = requests.get(settings.KATTIS_PROBLEM_URL + problemid, timeout=5)
|
|
42
|
-
if response.status_code == 200:
|
|
43
|
-
return response.text.strip()
|
|
44
|
-
else:
|
|
45
|
-
raise requests.exceptions.InvalidURL(f"Error: {response.status_code}")
|
|
43
|
+
return root_problem_folder
|
|
46
44
|
|
|
45
|
+
def download_html(self, problemid: str) -> str:
|
|
46
|
+
"""Download the problem HTML page for a given problem id.
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Returns the HTML body as a stripped string when successful or
|
|
49
|
+
raises an HTTP-related exception on failure.
|
|
50
|
+
"""
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
response = requests.get(
|
|
53
|
+
settings.KATTIS_PROBLEM_URL + problemid, timeout=5)
|
|
54
|
+
if response.status_code == 200:
|
|
55
|
+
return response.text.strip()
|
|
56
|
+
else:
|
|
57
|
+
raise requests.exceptions.InvalidURL(
|
|
58
|
+
f"Error: {response.status_code}")
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
def load_problem_metadata(self, problemid: str = '') -> Dict[Any, Any]:
|
|
61
|
+
"""Load problem metadata from disk or by scraping the problem page.
|
|
62
|
+
|
|
63
|
+
If a local YAML metadata file is present it is loaded. Otherwise
|
|
64
|
+
the problem page is downloaded and parsed, and the metadata is
|
|
65
|
+
saved locally before being returned.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
metadata: Dict[str, Any] = dict()
|
|
69
|
+
|
|
70
|
+
filename = "*.yaml"
|
|
71
|
+
if problemid:
|
|
72
|
+
filename = f"{problemid}.yaml"
|
|
73
|
+
try:
|
|
74
|
+
root_problem_folder = utility.find_problem_root_folder(
|
|
75
|
+
Path.cwd(), filename)
|
|
76
|
+
if not problemid:
|
|
77
|
+
problemid = root_problem_folder.name
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
if not problemid:
|
|
80
|
+
return metadata
|
|
81
|
+
else:
|
|
82
|
+
root_problem_folder = Path.cwd().joinpath(problemid)
|
|
83
|
+
if not root_problem_folder.exists():
|
|
84
|
+
root_problem_folder.mkdir()
|
|
85
|
+
|
|
86
|
+
metadata_file = root_problem_folder.joinpath(f"{problemid}.yaml")
|
|
87
|
+
if metadata_file.exists():
|
|
88
|
+
with open(metadata_file, "r", encoding='utf-8') as f:
|
|
89
|
+
metadata = yaml.safe_load(f)
|
|
90
|
+
if 'submissions' not in metadata:
|
|
91
|
+
metadata['submissions'] = 0
|
|
92
|
+
metadata['accepted'] = 0
|
|
93
|
+
config.update_problem_metadata(problemid, metadata)
|
|
72
94
|
else:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
html = self.download_html(problemid)
|
|
96
|
+
metadata = self._parse_metadata(problemid, html)
|
|
97
|
+
self.save_metadata(problemid, metadata)
|
|
98
|
+
return metadata
|
|
99
|
+
|
|
100
|
+
def _parse_metadata(self, problemid: str, html: str) -> Dict[str, Any]:
|
|
101
|
+
"""Internal parser that extracts metadata from problem HTML.
|
|
102
|
+
|
|
103
|
+
This method uses BeautifulSoup to extract title, limits,
|
|
104
|
+
difficulty and basic submission stats from the problem page.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
108
|
+
meta_data = {'problemid': problemid, 'title': '',
|
|
109
|
+
'cpu_limit': 'None', 'mem_limit': 'None',
|
|
110
|
+
'difficulty': 'None',
|
|
111
|
+
'submissions': 0, 'accepted': 0}
|
|
112
|
+
|
|
113
|
+
title = soup.find("h1")
|
|
114
|
+
if title:
|
|
115
|
+
meta_data["title"] = title.text
|
|
116
|
+
|
|
117
|
+
data = soup.find("div", {"class": "metadata-grid"})
|
|
118
|
+
name_mapping = {'metadata-cpu-card': 'cpu_limit',
|
|
119
|
+
'metadata-memmory-card': 'mem_limit',
|
|
120
|
+
'metadata-difficulty-card': 'difficulty',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for key, key1 in name_mapping.items():
|
|
124
|
+
try:
|
|
125
|
+
value = ''
|
|
126
|
+
# Use .find() instead of deprecated .findChild()
|
|
127
|
+
div = data.find('div', {'class': key}) # type: ignore
|
|
128
|
+
if key == 'metadata-difficulty-card':
|
|
129
|
+
cls = {'class': 'difficulty_number'}
|
|
130
|
+
span = div.find('span', cls) # type: ignore
|
|
131
|
+
if span:
|
|
132
|
+
value += span.text.strip() + ' ' # type: ignore
|
|
133
|
+
cls2 = {'class': 'text-blue-200'}
|
|
134
|
+
span = div.find('span', cls2) # type: ignore
|
|
135
|
+
if span:
|
|
136
|
+
value += span.text.strip() # type: ignore
|
|
137
|
+
meta_data[key1] = value
|
|
138
|
+
except AttributeError:
|
|
139
|
+
pass
|
|
140
|
+
return meta_data
|
|
141
|
+
|
|
142
|
+
def parse_metadata(self, problemid: str, html: str) -> Dict[str, Any]:
|
|
143
|
+
"""Public alias for the internal metadata parser.
|
|
144
|
+
|
|
145
|
+
Keeping a public method makes it easier for external callers and
|
|
146
|
+
the compatibility wrapper to access parsing without touching a
|
|
147
|
+
protected member.
|
|
148
|
+
"""
|
|
149
|
+
return self._parse_metadata(problemid, html)
|
|
150
|
+
|
|
151
|
+
def save_metadata(self, problemid: str, metadata: Dict[str, Any]) -> None:
|
|
152
|
+
"""Persist metadata dictionary to the problem YAML file."""
|
|
153
|
+
|
|
154
|
+
root_problem_folder = self.create_problem_folder(problemid)
|
|
155
|
+
metadata_file = root_problem_folder.joinpath(f"{problemid}.yaml")
|
|
156
|
+
with open(metadata_file, "w", encoding='utf-8') as f:
|
|
157
|
+
yaml.dump(metadata, f, default_flow_style=False,
|
|
158
|
+
allow_unicode=True)
|
|
159
|
+
|
|
160
|
+
def download_sample_data(self, problemid: str) -> None:
|
|
161
|
+
"""Download the sample data zip for a problem and unpack it.
|
|
162
|
+
|
|
163
|
+
The samples are downloaded into a `data/` folder inside the
|
|
164
|
+
problem root folder and the zip is removed after extraction.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
uri = f'{settings.KATTIS_PROBLEM_URL}{problemid}'
|
|
168
|
+
uri += '/file/statement/samples.zip'
|
|
169
|
+
|
|
170
|
+
root_problem_folder = self.create_problem_folder(problemid)
|
|
171
|
+
|
|
172
|
+
data_folder = root_problem_folder.joinpath('data')
|
|
173
|
+
data_folder.mkdir(exist_ok=True)
|
|
174
|
+
response = requests.get(uri, allow_redirects=True, timeout=10)
|
|
175
|
+
if response.status_code == 200:
|
|
176
|
+
zip_path = data_folder.joinpath('samples.zip')
|
|
177
|
+
with open(data_folder.joinpath('samples.zip'), 'wb') as f:
|
|
178
|
+
f.write(response.content)
|
|
179
|
+
shutil.unpack_archive(zip_path, data_folder)
|
|
180
|
+
zip_path.unlink()
|
|
181
|
+
else:
|
|
182
|
+
raise requests.exceptions.InvalidURL(
|
|
183
|
+
f"Error: {response.status_code}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Default manager instance for compatibility with existing imports
|
|
187
|
+
_manager = DownloadManager()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def create_problem_folder(problemid: str) -> Path:
|
|
191
|
+
"""Module-level wrapper returning the problem root folder.
|
|
192
|
+
|
|
193
|
+
Delegates to the default :class:`DownloadManager` instance so code
|
|
194
|
+
using the previous functional API continues to work.
|
|
106
195
|
"""
|
|
107
196
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
name_mapping = {'metadata-cpu-card': 'cpu_limit',
|
|
122
|
-
'metadata-memmory-card': 'mem_limit',
|
|
123
|
-
'metadata-difficulty-card': 'difficulty',
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
for key, key1 in name_mapping.items():
|
|
127
|
-
try:
|
|
128
|
-
value = ''
|
|
129
|
-
div = data.findChild('div', {'class': key}) # type: ignore
|
|
130
|
-
if key == 'metadata-difficulty-card':
|
|
131
|
-
span = div.findChild( # type: ignore
|
|
132
|
-
'span', {
|
|
133
|
-
'class': 'difficulty_number'})
|
|
134
|
-
value += span.text.strip() + ' ' # type: ignore
|
|
135
|
-
span = div.findChild('span', # type: ignore
|
|
136
|
-
{'class': 'text-blue-200'})
|
|
137
|
-
value += span.text.strip() # type: ignore
|
|
138
|
-
meta_data[key1] = value
|
|
139
|
-
except AttributeError:
|
|
140
|
-
pass
|
|
141
|
-
return meta_data
|
|
197
|
+
return _manager.create_problem_folder(problemid)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def download_html(problemid: str) -> str:
|
|
201
|
+
"""Download problem HTML using the module-level manager."""
|
|
202
|
+
|
|
203
|
+
return _manager.download_html(problemid)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def load_problem_metadata(problemid: str = '') -> Dict[Any, Any]:
|
|
207
|
+
"""Load problem metadata via the default DownloadManager."""
|
|
208
|
+
|
|
209
|
+
return _manager.load_problem_metadata(problemid)
|
|
142
210
|
|
|
143
211
|
|
|
144
212
|
def save_metadata(problemid: str, metadata: Dict[str, Any]) -> None:
|
|
145
|
-
"""
|
|
213
|
+
"""Persist metadata using the default DownloadManager."""
|
|
146
214
|
|
|
147
|
-
|
|
148
|
-
problemid (str): problemid
|
|
149
|
-
metadata (Dict[str, Any]): metadata
|
|
150
|
-
"""
|
|
151
|
-
root_problem_folder = create_problem_folder(problemid)
|
|
152
|
-
metadata_file = root_problem_folder.joinpath(f"{problemid}.yaml")
|
|
153
|
-
with open(metadata_file, "w", encoding='utf-8') as f:
|
|
154
|
-
yaml.dump(metadata, f, default_flow_style=False, allow_unicode=True)
|
|
215
|
+
return _manager.save_metadata(problemid, metadata)
|
|
155
216
|
|
|
156
217
|
|
|
157
218
|
def download_sample_data(problemid: str) -> None:
|
|
158
|
-
"""Download sample data
|
|
219
|
+
"""Download and unpack sample data using the default manager."""
|
|
220
|
+
|
|
221
|
+
return _manager.download_sample_data(problemid)
|
|
222
|
+
|
|
159
223
|
|
|
160
|
-
|
|
161
|
-
|
|
224
|
+
def _parse_metadata(problemid: str, html: str) -> Dict[str, Any]:
|
|
225
|
+
"""Compatibility wrapper for the legacy module-level helper used by
|
|
226
|
+
tests. Delegates to the DownloadManager implementation.
|
|
162
227
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
root_problem_folder = create_problem_folder(problemid)
|
|
167
|
-
|
|
168
|
-
data_folder = root_problem_folder.joinpath('data')
|
|
169
|
-
data_folder.mkdir(exist_ok=True)
|
|
170
|
-
response = requests.get(uri, allow_redirects=True, timeout=10)
|
|
171
|
-
if response.status_code == 200:
|
|
172
|
-
zip_path = data_folder.joinpath('samples.zip')
|
|
173
|
-
with open(data_folder.joinpath('samples.zip'), 'wb') as f:
|
|
174
|
-
f.write(response.content)
|
|
175
|
-
# extract zip file
|
|
176
|
-
shutil.unpack_archive(zip_path, data_folder)
|
|
177
|
-
zip_path.unlink()
|
|
178
|
-
else:
|
|
179
|
-
raise requests.exceptions.InvalidURL(f"Error: {response.status_code}")
|
|
228
|
+
# Prefer the public API on the manager to avoid accessing a protected
|
|
229
|
+
# member from outside the class.
|
|
230
|
+
return _manager.parse_metadata(problemid, html)
|
kattis_cli/fireworks.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Headless fireworks animation for CI/Docker.
|
|
3
|
+
|
|
4
|
+
This script intentionally avoids any GUI libraries and provides a
|
|
5
|
+
terminal-only fireworks animation suitable for headless environments.
|
|
6
|
+
Configuration can be provided via `scripts/ui_config.yml`
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import locale
|
|
12
|
+
import time
|
|
13
|
+
from random import randint, choice
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_fireworks() -> None:
|
|
19
|
+
"""Simple ASCII fireworks for headless environments.
|
|
20
|
+
|
|
21
|
+
Draws random bursts in the terminal using ANSI colors. Works in CI/Docker
|
|
22
|
+
and avoids external dependencies.
|
|
23
|
+
"""
|
|
24
|
+
frames = 40
|
|
25
|
+
width = 60
|
|
26
|
+
height = 20
|
|
27
|
+
message = None
|
|
28
|
+
symbols = ['*', '•', '✶', '✸']
|
|
29
|
+
colors = [
|
|
30
|
+
'\x1b[37m', # white
|
|
31
|
+
'\x1b[31m', # red
|
|
32
|
+
'\x1b[32m', # green
|
|
33
|
+
'\x1b[34m', # blue
|
|
34
|
+
'\x1b[33m', # yellow
|
|
35
|
+
'\x1b[35m', # magenta
|
|
36
|
+
'\x1b[36m', # cyan
|
|
37
|
+
]
|
|
38
|
+
reset = '\x1b[0m'
|
|
39
|
+
|
|
40
|
+
# Only emit ANSI color codes when stdout is a TTY and terminal
|
|
41
|
+
# appears to support colors. In CI or when output is captured the
|
|
42
|
+
# escape characters may be shown literally (e.g. as ``\u001b[...]``)
|
|
43
|
+
# so we disable color codes in that case to avoid escaped sequences
|
|
44
|
+
# appearing in logs.
|
|
45
|
+
use_color = sys.stdout.isatty() and os.environ.get('NO_COLOR') is None
|
|
46
|
+
if os.environ.get('TERM') == 'dumb':
|
|
47
|
+
use_color = False
|
|
48
|
+
if not use_color:
|
|
49
|
+
colors = ['']
|
|
50
|
+
reset = ''
|
|
51
|
+
|
|
52
|
+
# Detect Unicode support in the execution environment. Some minimal
|
|
53
|
+
# container images or redirected outputs use an ASCII locale so fancy
|
|
54
|
+
# symbols (✶, ✸, •, emojis) render poorly or get replaced. We fall
|
|
55
|
+
# back to ASCII symbols when Unicode is not supported.
|
|
56
|
+
out_encoding = sys.stdout.encoding or \
|
|
57
|
+
locale.getpreferredencoding(False) or ''
|
|
58
|
+
out_encoding = (out_encoding or '').lower()
|
|
59
|
+
supports_unicode = ('utf' in out_encoding)
|
|
60
|
+
if not supports_unicode:
|
|
61
|
+
# Simple ASCII fallback symbols
|
|
62
|
+
symbols = ['*', '+', 'o', '.']
|
|
63
|
+
# Replace message with ASCII-only fallback if necessary
|
|
64
|
+
if message:
|
|
65
|
+
try:
|
|
66
|
+
message.encode('ascii')
|
|
67
|
+
except (UnicodeEncodeError, TypeError):
|
|
68
|
+
message = (message.encode('ascii', 'replace')
|
|
69
|
+
.decode('ascii'))
|
|
70
|
+
# Inform interactive users why we fell back (but avoid noisy logs
|
|
71
|
+
# in captured/non-interactive runs).
|
|
72
|
+
if sys.stdout.isatty():
|
|
73
|
+
print(
|
|
74
|
+
'Note: terminal does not appear to support UTF-8 — '
|
|
75
|
+
'using ASCII fallback for fireworks.',
|
|
76
|
+
file=sys.stderr,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Hide cursor (only when terminal supports it)
|
|
81
|
+
if use_color:
|
|
82
|
+
sys.stdout.write('\x1b[?25l')
|
|
83
|
+
for _ in range(frames):
|
|
84
|
+
# clear screen when supported, otherwise don't emit escapes
|
|
85
|
+
if use_color:
|
|
86
|
+
sys.stdout.write('\x1b[2J\x1b[H')
|
|
87
|
+
cx = randint(width // 4, 3 * width // 4)
|
|
88
|
+
cy = randint(height // 4, 3 * height // 4)
|
|
89
|
+
grid = [[' ' for _ in range(width)] for _ in range(height)]
|
|
90
|
+
# place particles
|
|
91
|
+
for _ in range(randint(12, 40)):
|
|
92
|
+
dx = int(randint(-8, 8) * (randint(0, 100) / 100.0))
|
|
93
|
+
dy = int(randint(-4, 4) * (randint(0, 100) / 100.0))
|
|
94
|
+
x = max(0, min(width - 1, cx + dx))
|
|
95
|
+
y = max(0, min(height - 1, cy + dy))
|
|
96
|
+
grid[y][x] = choice(symbols)
|
|
97
|
+
|
|
98
|
+
# render
|
|
99
|
+
for row in grid:
|
|
100
|
+
line = ''
|
|
101
|
+
for ch in row:
|
|
102
|
+
if ch == ' ':
|
|
103
|
+
line += ' '
|
|
104
|
+
else:
|
|
105
|
+
line += choice(colors) + ch + reset
|
|
106
|
+
sys.stdout.write(line + '\n')
|
|
107
|
+
sys.stdout.flush()
|
|
108
|
+
time.sleep(0.12)
|
|
109
|
+
|
|
110
|
+
# final message
|
|
111
|
+
msg = message or '🎉 Congratulations — Accepted! 🎉'
|
|
112
|
+
sys.stdout.write('\n')
|
|
113
|
+
if use_color:
|
|
114
|
+
sys.stdout.write('\x1b[1;33m')
|
|
115
|
+
sys.stdout.write(msg)
|
|
116
|
+
sys.stdout.write('\x1b[0m\n')
|
|
117
|
+
else:
|
|
118
|
+
sys.stdout.write(msg + '\n')
|
|
119
|
+
sys.stdout.flush()
|
|
120
|
+
finally:
|
|
121
|
+
# show cursor again
|
|
122
|
+
if use_color:
|
|
123
|
+
sys.stdout.write('\x1b[?25h')
|
|
124
|
+
sys.stdout.flush()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == '__main__':
|
|
128
|
+
run_fireworks()
|