Skip to content

Commit 7cc5bfc

Browse files
authored
Add Support for Multiple Screens (#47)
* Add multi-screens option * Add tests for new methods * Bump Pytest to 8.3.5 * Drop support for Python 3.7, 3.8 * Add support for Python 3.10, 3.11, 3.12, 3.13
1 parent c666011 commit 7cc5bfc

File tree

7 files changed

+160
-13
lines changed

7 files changed

+160
-13
lines changed

.travis.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ dist: focal
55

66
language: python
77
python:
8-
- '3.7'
9-
- '3.8'
108
- '3.9'
9+
- '3.10'
10+
- '3.11'
11+
- '3.12'
12+
- '3.13'
1113

1214
install:
13-
- pip install setuptools==60.8.2
15+
- pip install setuptools==80.3.1
1416
- pip install -r requirements.txt
1517
- pip install .
1618

@@ -23,5 +25,5 @@ deploy:
2325
skip_existing: true
2426
edge: true
2527
on:
26-
python: 3.9
28+
python: 3.13
2729
tags: true

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
pytest==7.0.0
1+
pytest==8.3.5
22
requests==2.32.3

setup.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="Vestaboard",
8-
version="1.2.4",
8+
version="2.0.0",
99
author="Shane Sutro",
1010
author_email="shane@shanesutro.com",
1111
description="A Vestaboard Wrapper",
@@ -14,15 +14,13 @@
1414
url="https://github.com/ShaneSutro/Vestaboard.git",
1515
packages=setuptools.find_packages(),
1616
license="MIT",
17-
install_requires=[
18-
'requests'
19-
],
17+
install_requires=["requests"],
2018
classifiers=[
2119
"Programming Language :: Python :: 3 :: Only",
2220
"License :: OSI Approved :: MIT License",
2321
"Operating System :: OS Independent",
2422
"Development Status :: 5 - Production/Stable",
2523
"Intended Audience :: Developers",
2624
],
27-
python_requires='>=3.6',
28-
)
25+
python_requires=">=3.9",
26+
)

tests/test_formatting.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,11 @@ def test_convert_with_vestaboard_formatting():
210210
# fmt: on
211211

212212
assert t1 == e1
213-
assert t2 == e2
213+
assert t2 == e2
214+
215+
def test_create_screens_function():
216+
t1 = Formatter().createScreens("Let us go forth, the tellers of tales, and seize whatever prey the heart long for, and have no fear. - W.B. Yeats, in his finest hour of lucidity")
217+
218+
assert len(t1) == 2
219+
assert len(t1[0]) == 6
220+
assert len(t1[1]) == 6

tests/test_init.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ def test_large_board():
217217
assert large_board == expected
218218

219219

220+
def test_multi_screens():
221+
# fmt: off
222+
multiScreens = [[[12, 5, 20, 0, 21, 19, 0, 7, 15, 0, 6, 15, 18, 20, 8, 55, 0, 20, 8, 5, 0, 0], [20, 5, 12, 12, 5, 18, 19, 0, 15, 6, 0, 20, 1, 12, 5, 19, 55, 0, 1, 14, 4, 0], [19, 5, 9, 26, 5, 0, 23, 8, 1, 20, 5, 22, 5, 18, 0, 16, 18, 5, 25, 0, 0, 0], [20, 8, 5, 0, 8, 5, 1, 18, 20, 0, 12, 15, 14, 7, 0, 6, 15, 18, 55, 0, 0, 0], [1, 14, 4, 0, 8, 1, 22, 5, 0, 14, 15, 0, 6, 5, 1, 18, 56, 0, 44, 0, 0, 0], [23, 56, 2, 56, 0, 25, 5, 1, 20, 19, 55, 0, 9, 14, 0, 8, 9, 19, 0, 0, 0, 0]], [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [6, 9, 14, 5, 19, 20, 0, 8, 15, 21, 18, 0, 15, 6, 0, 0, 0, 0, 0, 0, 0, 0], [12, 21, 3, 9, 4, 9, 20, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]]
223+
# fmt: on
224+
vb = create_fake_vestaboard()
225+
vb.sendScreens(multiScreens, 1)
226+
227+
220228
def create_fake_vestaboard():
221229
i = vestaboard.Installable(
222230
apiKey=TEST_API_KEY,

vestaboard/__init__.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import warnings
1010
import json
1111
import requests
12+
import threading
13+
import time
1214
from vestaboard.formatter import Formatter
1315
import vestaboard.vbUrls as vbUrls
1416

@@ -213,7 +215,10 @@ def raw(self, charList: list, pad=None):
213215
if self.localKey and self.localIP:
214216
self._raw_local(finalText["characters"])
215217
elif self.readWrite and self.apiKey:
216-
headers = {"X-Vestaboard-Read-Write-Key": self.apiKey, "Content-Type": "application/json"}
218+
headers = {
219+
"X-Vestaboard-Read-Write-Key": self.apiKey,
220+
"Content-Type": "application/json",
221+
}
217222
requests.post(
218223
vbUrls.readWrite,
219224
headers=headers,
@@ -359,6 +364,74 @@ def _raw_local(self, chars):
359364
)
360365
res.raise_for_status()
361366

367+
def _sendScreensNonBlocking(self, screens, delay):
368+
for screenIndex in range(len(screens)):
369+
screen = screens[screenIndex]
370+
isLastScreen = screenIndex == len(screens) - 1
371+
for i, row in enumerate(screen):
372+
if not isinstance(row, list):
373+
raise ValueError(f"Nested items must be lists, not {type(row)}.")
374+
if len(row) != 22:
375+
raise ValueError(
376+
f"Nested lists must be exactly 22 characters long. Element at {i} is {len(row)} characters long. Use the Formatter().convertLine() function if you need to add padding to your row."
377+
)
378+
for j, char in enumerate(row):
379+
if not isinstance(char, int):
380+
raise ValueError(
381+
f"Nested lists must contain numbers only - check row {i} char {j} (0 indexed)"
382+
)
383+
if len(screen) > 6:
384+
# warnings.warn doesn't work with f strings
385+
warning_message = f"The Vestaboard API accepts only 6 lines of characters; you've passed in {len(screen)}. Only the first 6 will be shown."
386+
warnings.warn(warning_message)
387+
del screen[6:]
388+
389+
finalText = Formatter()._raw(screen)
390+
print(f"Sending screen {screenIndex + 1} of {len(screens)}")
391+
if self.localKey and self.localIP:
392+
self._raw_local(finalText["characters"])
393+
elif self.readWrite and self.apiKey:
394+
headers = {
395+
"X-Vestaboard-Read-Write-Key": self.apiKey,
396+
"Content-Type": "application/json",
397+
}
398+
requests.post(
399+
vbUrls.readWrite,
400+
headers=headers,
401+
data=json.dumps(finalText["characters"]),
402+
timeout=5,
403+
)
404+
else:
405+
headers = {
406+
"X-Vestaboard-Api-Key": self.apiKey,
407+
"X-Vestaboard-Api-Secret": self.apiSecret,
408+
}
409+
requests.post(
410+
vbUrls.post.format(self.subscriptionId),
411+
headers=headers,
412+
json=finalText,
413+
timeout=5,
414+
)
415+
if not isLastScreen:
416+
time.sleep(delay)
417+
418+
def sendScreens(self, screens, delay=60):
419+
"""
420+
Posts already-formatted screens to the board with a pre-set delay.
421+
Intended to be used with the Formatter.createScreens method. This method
422+
expects each screen to be a fully-sized list of lists.
423+
424+
Keyword arguments:
425+
426+
`screens` - a list of screens
427+
428+
`delay` - seconds to delay between screens. Defaults to 60 seconds.
429+
"""
430+
thread = threading.Thread(
431+
target=self._sendScreensNonBlocking, args=(screens, delay)
432+
)
433+
thread.start()
434+
362435

363436
class Installable:
364437
def __init__(

vestaboard/formatter.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,62 @@ def convertPlainText(
230230
convertedLines.insert(0, blankLine)
231231

232232
return convertedLines[0:maxRows]
233+
234+
def createScreens(
235+
self,
236+
text,
237+
size=None,
238+
justify="center",
239+
align="center",
240+
useVestaboardCentering=False,
241+
):
242+
"""
243+
This function returns multiple screens of text. This is particularly useful when the
244+
desired text is too big to fit on a single screen. The output is similar to the
245+
Formatter.convertPlainText method; the primary difference is the return shape, which
246+
is a nested list of lists.
247+
248+
The returned list size will match the `size` passed in and defaults to [6, 22].
249+
"""
250+
if size is None:
251+
size = [6, 22]
252+
maxRows, maxCols = size
253+
splitLines = []
254+
for linebreak in text.split("\n"):
255+
splitLines += textwrap.fill(linebreak, maxCols).split("\n")
256+
257+
convertedLines = []
258+
259+
if useVestaboardCentering:
260+
if justify == "center":
261+
warnings.warn(
262+
"Vestaboard formatting only affects left or right-justified text. Because of this, your text will be left justified by default. If you don't want your text left justified, remove `useVestaboardCentering` from your function call."
263+
)
264+
convertedLines = self._add_vestaboard_spacing(splitLines, size)
265+
else:
266+
convertedLines = self._add_vestaboard_spacing(splitLines, size, justify)
267+
else:
268+
for line in splitLines:
269+
convertedLines.append(self.convertLine(line, justify, "black", False))
270+
271+
screens = [
272+
convertedLines[i : i + maxRows]
273+
for i in range(0, len(convertedLines), maxRows)
274+
]
275+
276+
for screen in screens:
277+
if len(screen) < maxRows:
278+
blankLine = [0] * maxCols
279+
while len(screen) < maxRows:
280+
if align == "bottom":
281+
screen.insert(0, blankLine)
282+
else:
283+
screen.append(blankLine)
284+
285+
if len(screen) < maxRows:
286+
if align == "top":
287+
screen.append(blankLine)
288+
else:
289+
screen.insert(0, blankLine)
290+
291+
return screens

0 commit comments

Comments
 (0)