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.
Files changed (40) hide show
  1. base_loom_server/__init__.py +0 -0
  2. base_loom_server/app_runner.py +184 -0
  3. base_loom_server/base_loom_server.py +688 -0
  4. base_loom_server/base_mock_loom.py +241 -0
  5. base_loom_server/client_replies.py +108 -0
  6. base_loom_server/constants.py +6 -0
  7. base_loom_server/display.css +66 -0
  8. base_loom_server/display.html_template +102 -0
  9. base_loom_server/display.js +770 -0
  10. base_loom_server/example_loom_server.py +106 -0
  11. base_loom_server/example_mock_loom.py +71 -0
  12. base_loom_server/favicon-32x32.png +0 -0
  13. base_loom_server/locales/README.md +12 -0
  14. base_loom_server/locales/default.json +34 -0
  15. base_loom_server/locales/fr.json +34 -0
  16. base_loom_server/main.py +22 -0
  17. base_loom_server/mock_streams.py +156 -0
  18. base_loom_server/pattern_database.py +151 -0
  19. base_loom_server/py.typed +0 -0
  20. base_loom_server/reduced_pattern.py +283 -0
  21. base_loom_server/test_data/many color liftplan and zeros.dtx +67 -0
  22. base_loom_server/test_data/many color liftplan and zeros.wif +99 -0
  23. base_loom_server/test_data/many color multiple treadles and zeros.dtx +70 -0
  24. base_loom_server/test_data/many color multiple treadles and zeros.wif +106 -0
  25. base_loom_server/test_data/many color single treadles.dtx +69 -0
  26. base_loom_server/test_data/many color single treadles.wif +106 -0
  27. base_loom_server/test_data/two color liftplan.dtx +51 -0
  28. base_loom_server/test_data/two color liftplan.wif +70 -0
  29. base_loom_server/test_data/two color multiple treadles.dtx +54 -0
  30. base_loom_server/test_data/two color multiple treadles.wif +78 -0
  31. base_loom_server/test_data/two color single treadles.dtx +53 -0
  32. base_loom_server/test_data/two color single treadles.wif +78 -0
  33. base_loom_server/testutils.py +588 -0
  34. base_loom_server/version.py +3 -0
  35. base_loom_server-0.1.dist-info/LICENSE.txt +7 -0
  36. base_loom_server-0.1.dist-info/METADATA +79 -0
  37. base_loom_server-0.1.dist-info/RECORD +40 -0
  38. base_loom_server-0.1.dist-info/WHEEL +5 -0
  39. base_loom_server-0.1.dist-info/entry_points.txt +2 -0
  40. 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
+ )