ThreeDimensionModeller 1.0.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.
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env python
2
+ # =============================================================================
3
+ # ThreeDimensionModeller CLI — text menu and typed verbs.
4
+ # Law keys: requirement-python-cli-interface, requirement-python-cli-logging,
5
+ # requirement-python-tui, requirement-python-oop,
6
+ # requirement-python-about, requirement-domain-threedimensionmodeller,
7
+ # requirement-python-error-handling, requirement-python-coding-style,
8
+ # requirement-runtime-prerequisites
9
+ # Outlines are written for images in one chosen folder. There is no media join.
10
+ # CIAO-Lite: Caution • Intentional • Anti-fragile • Over-protect
11
+ # def main writes ChronicleLogger(...). This module does not construct it
12
+ # at import time and does not re-export it.
13
+ # =============================================================================
14
+ from __future__ import print_function, unicode_literals
15
+
16
+ import argparse
17
+ import sys
18
+
19
+ from .about_page import AboutPage
20
+ from .check_system import CheckSystem
21
+ from .self_management import SelfManage
22
+ from .tui import Tui
23
+
24
+
25
+ class Cli:
26
+ """Parser, dispatch, verbs, and the tty gate. One class in this file.
27
+
28
+ Identity, verb lists, and suffix sets are attributes of this class.
29
+ Collaborators arrive through the constructor. def main stays beside this class.
30
+ """
31
+
32
+ from . import __version__ as VERSION
33
+
34
+ APP_NAME = "ThreeDimensionModeller"
35
+ CONSOLE_NAME = "three-dimension-modeller"
36
+ PRODUCT_VERBS = (
37
+ "help",
38
+ "version",
39
+ "about",
40
+ "model",
41
+ "self-install",
42
+ "version-check",
43
+ "self-update",
44
+ "self-uninstall",
45
+ )
46
+ LIFECYCLE_VERBS = (
47
+ "version",
48
+ "self-install",
49
+ "version-check",
50
+ "self-update",
51
+ "self-uninstall",
52
+ )
53
+ AUTHOR_NAME = "Wilgat Wong"
54
+ AUTHOR_EMAIL = "wilgat.wong@gmail.com"
55
+ HOMEPAGE = "https://github.com/cloudgen/ThreeDimensionModeller"
56
+ LAST_UPDATE = "2026-10-05"
57
+ DOWNLOAD_URL = ""
58
+ BASIC_USAGE = "three-dimension-modeller model"
59
+ DOMAIN_SENTENCE = "Build a glTF model and an HTML viewer from outline images in a folder"
60
+
61
+ def __init__(self, logger=None):
62
+ self.logger = logger
63
+ if logger is not None:
64
+ logger.log_message("instantiated", component="Cli")
65
+ self.app_name = Cli.APP_NAME
66
+ self.version = Cli.VERSION
67
+ self.about = AboutPage(
68
+ CheckSystem(
69
+ logger=logger,
70
+ app_name=Cli.APP_NAME,
71
+ version=Cli.VERSION,
72
+ console_name=Cli.CONSOLE_NAME,
73
+ ),
74
+ Cli.APP_NAME,
75
+ Cli.VERSION,
76
+ Cli.AUTHOR_NAME,
77
+ Cli.LAST_UPDATE,
78
+ Cli.HOMEPAGE,
79
+ Cli.DOWNLOAD_URL,
80
+ Cli.BASIC_USAGE,
81
+ Cli.CONSOLE_NAME,
82
+ logger=logger,
83
+ )
84
+ self.self_manage = SelfManage(Cli.APP_NAME, Cli.VERSION, logger=logger)
85
+ self.tui = Tui(self, logger=logger)
86
+
87
+ @staticmethod
88
+ def stdout_is_tty():
89
+ """General Purpose: Whether the text screen can be drawn on this stdout."""
90
+ try:
91
+ return sys.stdout.isatty()
92
+ except Exception:
93
+ return False
94
+
95
+ @staticmethod
96
+ def opens_text_screen(argv):
97
+ """
98
+ General Purpose: Whether this argv draws the text screen.
99
+
100
+ The real parser has not run yet. This walk only decides is_quiet.
101
+ Help, version, model, and the pip verbs do not draw the screen.
102
+ Empty argv and about do, when stdout is a terminal.
103
+ model builds a glTF file and does not draw the screen.
104
+ """
105
+ if argv is None:
106
+ argv = sys.argv[1:]
107
+ argv = list(argv)
108
+ if "--help" in argv or "-h" in argv or "--version" in argv:
109
+ return False
110
+ if not Cli.stdout_is_tty():
111
+ return False
112
+ verb = None
113
+ saw_force = False
114
+ for token in argv:
115
+ if token == "--force":
116
+ saw_force = True
117
+ continue
118
+ if token.startswith("-"):
119
+ return False
120
+ if verb is None:
121
+ verb = token
122
+ continue
123
+ return False
124
+ if saw_force and verb != "self-uninstall":
125
+ return False
126
+ if verb == "about":
127
+ return True
128
+ return verb is None
129
+
130
+ def report_error(self, message, nxt):
131
+ """User-visible failure plus the same fact on the logger when one exists."""
132
+ if self.logger is not None:
133
+ self.logger.log_message(
134
+ "{0} Next: {1}".format(message, nxt),
135
+ level="ERROR",
136
+ component="main",
137
+ )
138
+ print("ERROR: {0}".format(message), file=sys.stderr)
139
+ print(" Next: {0}".format(nxt), file=sys.stderr)
140
+ return 1
141
+
142
+ def build_parser(self):
143
+ """
144
+ General Purpose: One optional product verb. No file-operand flags.
145
+ help is also --help. --force belongs only to self-uninstall.
146
+ """
147
+ parser = argparse.ArgumentParser(
148
+ prog=Cli.CONSOLE_NAME,
149
+ formatter_class=argparse.RawDescriptionHelpFormatter,
150
+ description=(
151
+ "{0} — build a glTF model and an HTML viewer from outline images.\n"
152
+ "With no arguments on a terminal, opens the text menu.\n"
153
+ "With no arguments and no terminal, prints this help and stops.\n"
154
+ "Product verbs: help, version, about, model, self-install,\n"
155
+ "version-check, self-update, self-uninstall.\n"
156
+ "help prints this usage. version prints the installed version.\n"
157
+ "about prints a page. model reads one folder and does not\n"
158
+ "draw the menu. It writes model.glb and viewer.html in that\n"
159
+ "folder's model directory. Menu row 1 asks for the current\n"
160
+ "folder or a subfolder.\n"
161
+ "model with no folder uses the current directory.\n"
162
+ "version-check runs: python -m pip index versions {0}\n"
163
+ "self-update runs: python -m pip install --upgrade {0}\n"
164
+ "self-install runs: python -m pip install {0}\n"
165
+ "self-uninstall runs: python -m pip uninstall -y {0}\n"
166
+ "and needs --force. Empty arguments do not install or update."
167
+ .format(Cli.APP_NAME)
168
+ ),
169
+ )
170
+ parser.add_argument(
171
+ "verb",
172
+ nargs="?",
173
+ default=None,
174
+ metavar="verb",
175
+ help=(
176
+ "Product verb: help, version, about, model, "
177
+ "self-install, version-check, self-update, or self-uninstall"
178
+ ),
179
+ )
180
+ parser.add_argument(
181
+ "target",
182
+ nargs="?",
183
+ default=None,
184
+ metavar="folder",
185
+ help="Folder of outline images for model. Default is the current directory.",
186
+ )
187
+ parser.add_argument(
188
+ "--views",
189
+ default=None,
190
+ help="JSON file of azimuth and elevation for model. Only on model.",
191
+ )
192
+ parser.add_argument(
193
+ "--grid",
194
+ type=int,
195
+ default=None,
196
+ help="Voxel count on each axis for model. Only on model. Default is 96.",
197
+ )
198
+ parser.add_argument(
199
+ "--output",
200
+ default=None,
201
+ help="Directory for model.glb and viewer.html. Only on model. Default: <folder>/model.",
202
+ )
203
+ parser.add_argument(
204
+ "--version",
205
+ action="version",
206
+ version="{0} {1}".format(Cli.APP_NAME, Cli.VERSION),
207
+ )
208
+ parser.add_argument(
209
+ "--force",
210
+ action="store_true",
211
+ help="Confirm self-uninstall. Required on the command line.",
212
+ )
213
+ return parser
214
+
215
+ def _page(self, text):
216
+ print(text)
217
+ return 0
218
+
219
+ def _verb_model(self, target, views, grid, output):
220
+ """Build one model. Does not draw the menu and does not prompt.
221
+
222
+ requirement-python-cli-interface — the waiting sentence is flushed
223
+ before the work. The returned lines still start with that sentence.
224
+ This method prints only the remainder.
225
+ """
226
+ from .model import convert_folder
227
+
228
+ folder = target if target else "."
229
+ spoken = []
230
+
231
+ def _notice(line):
232
+ print(line, flush=True)
233
+ spoken.append(line)
234
+
235
+ code, lines = convert_folder(
236
+ folder,
237
+ output_dir=output,
238
+ views_path=views,
239
+ grid=grid,
240
+ choice="model",
241
+ on_notice=_notice,
242
+ )
243
+ rest = lines[len(spoken):]
244
+ if rest:
245
+ print("\n".join(rest))
246
+ return code
247
+
248
+ def _unknown_verb(self, token):
249
+ names = ", ".join(Cli.PRODUCT_VERBS)
250
+ return self.report_error(
251
+ "Unknown verb '{0}'.".format(token),
252
+ "{0} help — verbs: {1}".format(Cli.CONSOLE_NAME, names),
253
+ )
254
+
255
+ def _dispatch(self, args):
256
+ """One product verb, the front board, or a fail-closed stop."""
257
+ verb = args.verb
258
+ if args.force and verb != "self-uninstall":
259
+ return self.report_error(
260
+ "--force is only for self-uninstall.",
261
+ "{0} self-uninstall --force".format(Cli.CONSOLE_NAME),
262
+ )
263
+ if (args.views or args.grid is not None or args.output) and verb != "model":
264
+ return self.report_error(
265
+ "--views, --grid, and --output are only for model.",
266
+ "{0} model --grid 96".format(Cli.CONSOLE_NAME),
267
+ )
268
+ if args.target and verb != "model":
269
+ return self.report_error(
270
+ "A folder is only for model.",
271
+ "{0} model".format(Cli.CONSOLE_NAME),
272
+ )
273
+ if verb == "about":
274
+ result = self.tui.about_on_screen()
275
+ if isinstance(result, int):
276
+ return result
277
+ return self._page(result)
278
+ if verb == "model":
279
+ return self._verb_model(args.target, args.views, args.grid, args.output)
280
+ if verb == "version":
281
+ return self._page(self.self_manage.local_version())
282
+ if verb == "self-uninstall":
283
+ if not args.force:
284
+ return self.report_error(
285
+ "self-uninstall removes this package with pip.",
286
+ "{0} self-uninstall --force".format(Cli.CONSOLE_NAME),
287
+ )
288
+ return self.self_manage.emit(verb)
289
+ if verb in ("version-check", "self-update", "self-install"):
290
+ return self.self_manage.emit(verb)
291
+ if verb is not None:
292
+ return self._unknown_verb(verb)
293
+ if not self.stdout_is_tty():
294
+ self.build_parser().print_help()
295
+ return 0
296
+ action = self.tui.open_text_menu()
297
+ if action == "missing":
298
+ return 1
299
+ return 0
300
+
301
+ def run(self, argv=None):
302
+ """One job. def main already wrote ChronicleLogger(...) and passed it in."""
303
+ if argv is None:
304
+ argv = sys.argv[1:]
305
+ argv = list(argv)
306
+ parser = self.build_parser()
307
+ try:
308
+ args = parser.parse_args(argv)
309
+ except SystemExit as exc:
310
+ code = exc.code
311
+ if code is None or code == 0:
312
+ return 0
313
+ return int(code)
314
+ if args.verb == "help":
315
+ parser.print_help()
316
+ return 0
317
+ return self._dispatch(args)
318
+
319
+
320
+ def main(argv=None, basedir="", logdir=""):
321
+ """This function instantiates ChronicleLogger. The statement below is the construct."""
322
+ if argv is None:
323
+ argv = sys.argv[1:]
324
+ argv = list(argv)
325
+ try:
326
+ from ChronicleLogger import ChronicleLogger
327
+ except ImportError:
328
+ sys.stderr.write(
329
+ 'ChronicleLogger is required. Next step: python -m pip install "ChronicleLogger>=1.3.1"\n'
330
+ )
331
+ return 1
332
+
333
+ from ThreeDimensionModeller import __version__
334
+
335
+ screen = Cli.opens_text_screen(argv)
336
+ logger = ChronicleLogger(
337
+ logname=Cli.APP_NAME,
338
+ is_quiet=screen,
339
+ basedir=basedir,
340
+ logdir=logdir,
341
+ )
342
+ appname = logger.logName()
343
+ resolved_base = logger.baseDir()
344
+ logger.logDir()
345
+
346
+ if logger.isDebug():
347
+ logger.log_message(
348
+ "{0} v{1} ({2})".format(appname, __version__, __file__),
349
+ component="main",
350
+ )
351
+ logger.log_message(
352
+ "Using {0}".format(ChronicleLogger.class_version()),
353
+ component="main",
354
+ )
355
+ logger.log_message(
356
+ "Base {0}".format(resolved_base),
357
+ level="DEBUG",
358
+ component="main",
359
+ )
360
+ logger.log_message("debug mode", component="main")
361
+
362
+ app = Cli(logger)
363
+ return app.run(argv)
364
+
365
+
366
+ if __name__ == "__main__":
367
+ sys.exit(main())