base-loom-server 0.1__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.
- base_loom_server/__init__.py +0 -0
- base_loom_server/app_runner.py +184 -0
- base_loom_server/base_loom_server.py +688 -0
- base_loom_server/base_mock_loom.py +241 -0
- base_loom_server/client_replies.py +108 -0
- base_loom_server/constants.py +6 -0
- base_loom_server/display.css +66 -0
- base_loom_server/display.html_template +102 -0
- base_loom_server/display.js +770 -0
- base_loom_server/example_loom_server.py +106 -0
- base_loom_server/example_mock_loom.py +71 -0
- base_loom_server/favicon-32x32.png +0 -0
- base_loom_server/locales/README.md +12 -0
- base_loom_server/locales/default.json +34 -0
- base_loom_server/locales/fr.json +34 -0
- base_loom_server/main.py +22 -0
- base_loom_server/mock_streams.py +156 -0
- base_loom_server/pattern_database.py +151 -0
- base_loom_server/py.typed +0 -0
- base_loom_server/reduced_pattern.py +283 -0
- base_loom_server/test_data/many color liftplan and zeros.dtx +67 -0
- base_loom_server/test_data/many color liftplan and zeros.wif +99 -0
- base_loom_server/test_data/many color multiple treadles and zeros.dtx +70 -0
- base_loom_server/test_data/many color multiple treadles and zeros.wif +106 -0
- base_loom_server/test_data/many color single treadles.dtx +69 -0
- base_loom_server/test_data/many color single treadles.wif +106 -0
- base_loom_server/test_data/two color liftplan.dtx +51 -0
- base_loom_server/test_data/two color liftplan.wif +70 -0
- base_loom_server/test_data/two color multiple treadles.dtx +54 -0
- base_loom_server/test_data/two color multiple treadles.wif +78 -0
- base_loom_server/test_data/two color single treadles.dtx +53 -0
- base_loom_server/test_data/two color single treadles.wif +78 -0
- base_loom_server/testutils.py +588 -0
- base_loom_server/version.py +3 -0
- base_loom_server-0.1.dist-info/LICENSE.txt +7 -0
- base_loom_server-0.1.dist-info/METADATA +79 -0
- base_loom_server-0.1.dist-info/RECORD +40 -0
- base_loom_server-0.1.dist-info/WHEEL +5 -0
- base_loom_server-0.1.dist-info/entry_points.txt +2 -0
- base_loom_server-0.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib.resources
|
|
3
|
+
import json
|
|
4
|
+
import locale
|
|
5
|
+
import logging
|
|
6
|
+
import pathlib
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from typing import AsyncGenerator, Type
|
|
9
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
from fastapi import APIRouter, FastAPI, WebSocket
|
|
12
|
+
from fastapi.responses import HTMLResponse, Response
|
|
13
|
+
|
|
14
|
+
from .base_loom_server import DEFAULT_DATABASE_PATH, BaseLoomServer
|
|
15
|
+
from .constants import LOG_NAME
|
|
16
|
+
|
|
17
|
+
PKG_FILES = importlib.resources.files("base_loom_server")
|
|
18
|
+
LOCALE_FILES = PKG_FILES.joinpath("locales")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AppRunner:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
app: FastAPI,
|
|
25
|
+
server_class: Type[BaseLoomServer],
|
|
26
|
+
favicon: bytes,
|
|
27
|
+
app_package_name: str,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Construct endpoints for FastAPI"""
|
|
30
|
+
self.log = logging.getLogger(LOG_NAME)
|
|
31
|
+
|
|
32
|
+
self.server_class = server_class
|
|
33
|
+
self.favicon = favicon
|
|
34
|
+
self.app_package_name = app_package_name
|
|
35
|
+
self.loom_server: BaseLoomServer | None = None
|
|
36
|
+
self.translation_dict: dict[str, str] = {}
|
|
37
|
+
|
|
38
|
+
# There must be a better way to do this,
|
|
39
|
+
# but everything I have tried fails,
|
|
40
|
+
# including using an APIRouter with add_api_route
|
|
41
|
+
|
|
42
|
+
@asynccontextmanager
|
|
43
|
+
async def lifespan_wrapper(*args):
|
|
44
|
+
async with self.lifespan(app):
|
|
45
|
+
yield
|
|
46
|
+
|
|
47
|
+
# The only rason we need a router is to set the lifespan
|
|
48
|
+
# but once we have it we may as well use it to add endpoints as well
|
|
49
|
+
router = APIRouter(lifespan=lifespan_wrapper)
|
|
50
|
+
|
|
51
|
+
@router.get("/")
|
|
52
|
+
async def get_wrapper():
|
|
53
|
+
return await self.get()
|
|
54
|
+
|
|
55
|
+
@router.get("/favicon.ico", include_in_schema=False)
|
|
56
|
+
async def get_favicon():
|
|
57
|
+
return await self.get_favicon()
|
|
58
|
+
|
|
59
|
+
@router.websocket("/ws")
|
|
60
|
+
async def websocket_endpoint_wrapper(websocket: WebSocket):
|
|
61
|
+
return await self.websocket_endpoint(websocket)
|
|
62
|
+
|
|
63
|
+
app.include_router(router)
|
|
64
|
+
|
|
65
|
+
def create_argument_parser(self) -> argparse.ArgumentParser:
|
|
66
|
+
parser = argparse.ArgumentParser()
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"serial_port",
|
|
69
|
+
help="Serial port connected to the loom, "
|
|
70
|
+
"typically of the form /dev/tty... "
|
|
71
|
+
"Specify 'mock' to run a mock (simulated) loom",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"-n",
|
|
75
|
+
"--name",
|
|
76
|
+
help="loom name",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"-r",
|
|
80
|
+
"--reset-db",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="reset pattern database?",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"-v",
|
|
86
|
+
"--verbose",
|
|
87
|
+
action="store_true",
|
|
88
|
+
help="print diagnostic information to stdout",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--db-path",
|
|
92
|
+
default=DEFAULT_DATABASE_PATH,
|
|
93
|
+
type=pathlib.Path,
|
|
94
|
+
help="Path for pattern database. "
|
|
95
|
+
"Settable so unit tests can avoid changing the real database.",
|
|
96
|
+
)
|
|
97
|
+
return parser
|
|
98
|
+
|
|
99
|
+
@asynccontextmanager
|
|
100
|
+
async def lifespan(self, app: FastAPI) -> AsyncGenerator[None, FastAPI]:
|
|
101
|
+
self.translation_dict = self.get_translation_dict()
|
|
102
|
+
|
|
103
|
+
parser = self.create_argument_parser()
|
|
104
|
+
args = parser.parse_args()
|
|
105
|
+
|
|
106
|
+
async with self.server_class(
|
|
107
|
+
**vars(args), translation_dict=self.translation_dict
|
|
108
|
+
) as self.loom_server:
|
|
109
|
+
yield
|
|
110
|
+
|
|
111
|
+
def get_translation_dict(self) -> dict[str, str]:
|
|
112
|
+
"""Get the translation dict for the current locale"""
|
|
113
|
+
# Read a dict of key: None and turn into a dict of key: key
|
|
114
|
+
default_dict = json.loads(LOCALE_FILES.joinpath("default.json").read_text())
|
|
115
|
+
translation_dict = {key: key for key in default_dict}
|
|
116
|
+
|
|
117
|
+
language_code = locale.getlocale(locale.LC_CTYPE)[0]
|
|
118
|
+
self.log.info(f"Locale: {language_code!r}")
|
|
119
|
+
if language_code is not None:
|
|
120
|
+
short_language_code = language_code.split("_")[0]
|
|
121
|
+
for lc in (short_language_code, language_code):
|
|
122
|
+
translation_name = lc + ".json"
|
|
123
|
+
translation_file = LOCALE_FILES.joinpath(translation_name)
|
|
124
|
+
if translation_file.is_file():
|
|
125
|
+
self.log.info(f"Loading translation file {translation_name!r}")
|
|
126
|
+
locale_dict = json.loads(translation_file.read_text())
|
|
127
|
+
purged_locale_dict = {
|
|
128
|
+
key: value
|
|
129
|
+
for key, value in locale_dict.items()
|
|
130
|
+
if value is not None
|
|
131
|
+
}
|
|
132
|
+
if purged_locale_dict != locale_dict:
|
|
133
|
+
self.log.warning(
|
|
134
|
+
f"Some entries in translation file {translation_name!r} "
|
|
135
|
+
"have null entries"
|
|
136
|
+
)
|
|
137
|
+
translation_dict.update(purged_locale_dict)
|
|
138
|
+
return translation_dict
|
|
139
|
+
|
|
140
|
+
async def get(self) -> HTMLResponse:
|
|
141
|
+
display_html_template = PKG_FILES.joinpath("display.html_template").read_text()
|
|
142
|
+
|
|
143
|
+
display_css = PKG_FILES.joinpath("display.css").read_text()
|
|
144
|
+
|
|
145
|
+
display_js = PKG_FILES.joinpath("display.js").read_text()
|
|
146
|
+
js_translation_str = "const TranslationDict = " + json.dumps(
|
|
147
|
+
self.translation_dict, indent=4
|
|
148
|
+
)
|
|
149
|
+
display_js = display_js.replace(
|
|
150
|
+
"const TranslationDict = {}", js_translation_str
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
assert self.loom_server is not None
|
|
154
|
+
is_mock = self.loom_server.mock_loom is not None
|
|
155
|
+
display_debug_controls = "block" if is_mock else "none"
|
|
156
|
+
|
|
157
|
+
display_html = display_html_template.format(
|
|
158
|
+
display_css=display_css,
|
|
159
|
+
display_js=display_js,
|
|
160
|
+
display_debug_controls=display_debug_controls,
|
|
161
|
+
**self.translation_dict,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return HTMLResponse(display_html)
|
|
165
|
+
|
|
166
|
+
async def get_favicon(self) -> Response:
|
|
167
|
+
return Response(content=self.favicon, media_type="image/x-icon")
|
|
168
|
+
|
|
169
|
+
async def websocket_endpoint(self, websocket: WebSocket) -> None:
|
|
170
|
+
assert self.loom_server is not None
|
|
171
|
+
await self.loom_server.run_client(websocket=websocket)
|
|
172
|
+
|
|
173
|
+
def run(self, host="0.0.0.0", port=8000, log_level="info", reload=True) -> None:
|
|
174
|
+
# Handle the help argument and also catch parsing errors right away
|
|
175
|
+
arg_parser = self.create_argument_parser()
|
|
176
|
+
arg_parser.parse_args()
|
|
177
|
+
|
|
178
|
+
uvicorn.run(
|
|
179
|
+
self.app_package_name,
|
|
180
|
+
host=host,
|
|
181
|
+
port=port,
|
|
182
|
+
log_level=log_level,
|
|
183
|
+
reload=reload,
|
|
184
|
+
)
|