Nexom 0.1.4__py3-none-any.whl → 1.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 (67) hide show
  1. nexom/__init__.py +2 -2
  2. nexom/__main__.py +111 -17
  3. nexom/app/__init__.py +62 -0
  4. nexom/app/auth.py +322 -0
  5. nexom/{web → app}/cookie.py +4 -2
  6. nexom/app/db.py +88 -0
  7. nexom/app/path.py +195 -0
  8. nexom/app/request.py +267 -0
  9. nexom/{web → app}/response.py +13 -3
  10. nexom/{web → app}/template.py +1 -1
  11. nexom/app/user.py +31 -0
  12. nexom/assets/app/__init__.py +0 -0
  13. nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
  14. nexom/assets/app/config.py +28 -0
  15. nexom/assets/app/gunicorn.conf.py +5 -0
  16. nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
  17. nexom/assets/app/pages/_templates.py +7 -0
  18. nexom/assets/{server → app}/pages/default.py +2 -2
  19. nexom/assets/{server → app}/pages/document.py +2 -2
  20. nexom/assets/app/router.py +12 -0
  21. nexom/assets/app/wsgi.py +64 -0
  22. nexom/assets/auth/__init__.py +0 -0
  23. nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
  24. nexom/assets/auth/config.py +27 -0
  25. nexom/assets/auth/gunicorn.conf.py +5 -0
  26. nexom/assets/auth/wsgi.py +62 -0
  27. nexom/assets/auth_page/login.html +95 -0
  28. nexom/assets/auth_page/signup.html +106 -0
  29. nexom/assets/error_page/error.html +3 -3
  30. nexom/assets/gateway/apache_app.conf +16 -0
  31. nexom/assets/gateway/nginx_app.conf +21 -0
  32. nexom/buildTools/__init__.py +1 -0
  33. nexom/buildTools/build.py +274 -54
  34. nexom/buildTools/run.py +185 -0
  35. nexom/core/__init__.py +2 -1
  36. nexom/core/error.py +81 -3
  37. nexom/core/log.py +111 -0
  38. nexom/{engine → core}/object_html_render.py +4 -1
  39. nexom/templates/__init__.py +0 -0
  40. nexom/templates/auth.py +72 -0
  41. {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/METADATA +75 -50
  42. nexom-1.0.1.dist-info/RECORD +56 -0
  43. {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/WHEEL +1 -1
  44. nexom/assets/server/config.py +0 -27
  45. nexom/assets/server/gunicorn.conf.py +0 -16
  46. nexom/assets/server/pages/__pycache__/__init__.cpython-313.pyc +0 -0
  47. nexom/assets/server/pages/_templates.py +0 -11
  48. nexom/assets/server/router.py +0 -18
  49. nexom/assets/server/wsgi.py +0 -30
  50. nexom/engine/__init__.py +0 -1
  51. nexom/web/__init__.py +0 -5
  52. nexom/web/path.py +0 -125
  53. nexom/web/request.py +0 -62
  54. nexom-0.1.4.dist-info/RECORD +0 -39
  55. /nexom/{web → app}/http_status_codes.py +0 -0
  56. /nexom/{web → app}/middleware.py +0 -0
  57. /nexom/assets/{server → app}/pages/__init__.py +0 -0
  58. /nexom/assets/{server → app}/static/dog.jpeg +0 -0
  59. /nexom/assets/{server → app}/static/style.css +0 -0
  60. /nexom/assets/{server → app}/templates/base.html +0 -0
  61. /nexom/assets/{server → app}/templates/default.html +0 -0
  62. /nexom/assets/{server → app}/templates/document.html +0 -0
  63. /nexom/assets/{server → app}/templates/footer.html +0 -0
  64. /nexom/assets/{server → app}/templates/header.html +0 -0
  65. {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/entry_points.txt +0 -0
  66. {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/licenses/LICENSE +0 -0
  67. {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/top_level.txt +0 -0
nexom/buildTools/build.py CHANGED
@@ -1,99 +1,319 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from importlib import resources
4
+ from importlib import import_module, resources
5
5
  from pathlib import Path
6
+ import re
6
7
  import shutil
7
8
 
8
9
 
9
10
  @dataclass(frozen=True)
10
- class ServerBuildOptions:
11
- """Options used to fill generated config.py."""
11
+ class AppBuildOptions:
12
12
  address: str = "0.0.0.0"
13
13
  port: int = 8080
14
14
  workers: int = 4
15
15
  reload: bool = False
16
16
 
17
17
 
18
+ _NAME_RE = re.compile(r"^[A-Za-z0-9_]+$")
19
+
20
+
21
+ class AppBuildError(RuntimeError):
22
+ """Raised when project generation fails for any reason."""
23
+
24
+
18
25
  def _copy_from_package(pkg: str, filename: str, dest: Path) -> None:
19
- """
20
- Copy a file from a package resource into the destination path.
21
- """
22
26
  dest.parent.mkdir(parents=True, exist_ok=True)
23
- with resources.files(pkg).joinpath(filename).open("rb") as src, dest.open("wb") as dst:
27
+ module = import_module(pkg)
28
+ with resources.files(module).joinpath(filename).open("rb") as src, dest.open("wb") as dst:
24
29
  shutil.copyfileobj(src, dst)
25
30
 
26
31
 
27
- def server(work_dir: str | Path, name: str, *, options: ServerBuildOptions | None = None) -> Path:
28
- """
29
- Generate a Nexom server project into `work_dir`.
32
+ def _read_text_from_package(pkg: str, filename: str) -> str:
33
+ module = import_module(pkg)
34
+ return resources.files(module).joinpath(filename).read_text(encoding="utf-8")
30
35
 
31
- This function copies template files bundled in the package (assets) and
32
- writes a ready-to-run config.py.
33
36
 
34
- Args:
35
- work_dir: Output directory where project files are created.
36
- name: Project name (reserved for future use; currently not used).
37
- options: Config defaults for generated config.py.
37
+ def _replace_many(text: str, repl: dict[str, str]) -> str:
38
+ out = text
39
+ for k, v in repl.items():
40
+ out = out.replace(k, v)
38
41
 
39
- Returns:
40
- The absolute path to the generated project directory.
42
+ unresolved = [k for k in repl.keys() if k in out]
43
+ if unresolved:
44
+ raise AppBuildError("Build template placeholder was not resolved.")
45
+ return out
46
+
47
+
48
+ def _validate_app_name(name: str) -> None:
49
+ if not _NAME_RE.match(name):
50
+ raise ValueError("name must match [A-Za-z0-9_]+ (no dots, slashes, hyphens).")
41
51
 
42
- Raises:
43
- FileExistsError: If target directories/files already exist.
44
- ModuleNotFoundError / FileNotFoundError: If bundled assets are missing.
45
- """
46
- _ = name # reserved (keep signature stable for future)
47
- options = options or ServerBuildOptions()
48
52
 
49
- out_dir = Path(work_dir).expanduser().resolve()
50
- out_dir.mkdir(parents=True, exist_ok=True)
53
+ def _write_gateway_config(
54
+ gateway_dir: Path,
55
+ *,
56
+ kind: str, # "nginx" | "apache"
57
+ app_name: str,
58
+ app_port: int,
59
+ domain: str,
60
+ ) -> Path:
61
+ if kind not in ("nginx", "apache"):
62
+ raise ValueError("kind must be nginx or apache.")
51
63
 
52
- pages_dir = out_dir / "pages"
53
- templates_dir = out_dir / "templates"
54
- static_dir = out_dir / "static"
64
+ # domain placeholder
65
+ dom = domain.strip() or "YOUR_DOMAIN_HERE # set --domain example.com"
55
66
 
56
- # Make sure we don't accidentally overwrite a project
57
- for d in (pages_dir, templates_dir, static_dir):
58
- if d.exists():
59
- raise FileExistsError(f"Already exists: {d}")
67
+ # wsgi import path (project root is PYTHONPATH in naxom run)
68
+ wsgi_target = f"{app_name}.wsgi:app"
69
+
70
+ pkg = "nexom.assets.gateway"
71
+ filename = "nginx_app.conf" if kind == "nginx" else "apache_app.conf"
72
+
73
+ text = _read_text_from_package(pkg, filename)
74
+ out = _replace_many(
75
+ text,
76
+ {
77
+ "__DOMAIN__": dom,
78
+ "__APP_PORT__": str(app_port),
79
+ "__APP_WSGI__": wsgi_target,
80
+ "__APP_NAME__": app_name,
81
+ },
82
+ )
60
83
 
84
+ gateway_dir.mkdir(parents=True, exist_ok=True)
85
+ out_path = gateway_dir / f"{app_name}.{kind}.conf"
86
+ out_path.write_text(out, encoding="utf-8")
87
+ return out_path
88
+
89
+
90
+ def create_app(
91
+ project_dir: str | Path,
92
+ app_name: str,
93
+ *,
94
+ options: AppBuildOptions | None = None,
95
+ gateway_config: str | None = None,
96
+ domain: str = "",
97
+ ) -> Path:
98
+ _validate_app_name(app_name)
99
+ options = options or AppBuildOptions()
100
+
101
+ project_root = Path(project_dir).expanduser().resolve()
102
+ project_root.mkdir(parents=True, exist_ok=True)
103
+
104
+ app_root = project_root / app_name
105
+ if app_root.exists():
106
+ raise FileExistsError(f"Target app already exists: {app_root}")
107
+ app_root.mkdir(parents=True, exist_ok=False)
108
+
109
+ pages_dir = app_root / "pages"
110
+ templates_dir = app_root / "templates"
111
+ static_dir = app_root / "static"
61
112
  pages_dir.mkdir()
62
113
  templates_dir.mkdir()
63
114
  static_dir.mkdir()
64
115
 
65
- # ---- Copy pages ----
66
- pages_pkg = "nexom.assets.server.pages"
116
+ # pages
117
+ pages_pkg = "nexom.assets.app.pages"
67
118
  for fn in ("__init__.py", "_templates.py", "default.py", "document.py"):
68
119
  _copy_from_package(pages_pkg, fn, pages_dir / fn)
69
120
 
70
- # ---- Copy templates ----
71
- templates_pkg = "nexom.assets.server.templates"
121
+ # templates
122
+ templates_pkg = "nexom.assets.app.templates"
72
123
  for fn in ("base.html", "header.html", "footer.html", "default.html", "document.html"):
73
124
  _copy_from_package(templates_pkg, fn, templates_dir / fn)
74
125
 
75
- # ---- Copy static ----
76
- static_pkg = "nexom.assets.server.static"
126
+ # static
127
+ static_pkg = "nexom.assets.app.static"
77
128
  for fn in ("dog.jpeg", "style.css"):
78
129
  _copy_from_package(static_pkg, fn, static_dir / fn)
79
130
 
80
- # ---- Copy app files ----
81
- app_pkg = "nexom.assets.server"
82
- for fn in ("gunicorn.conf.py", "router.py", "wsgi.py", "config.py"):
83
- _copy_from_package(app_pkg, fn, out_dir / fn)
131
+ # app files
132
+ app_pkg = "nexom.assets.app"
133
+ for fn in ("__init__.py", "gunicorn.conf.py", "router.py", "wsgi.py", "config.py"):
134
+ _copy_from_package(app_pkg, fn, app_root / fn)
84
135
 
85
- # ---- Enable settings (format config.py) ----
86
- config_path = out_dir / "config.py"
136
+ # config.py
137
+ config_path = app_root / "config.py"
87
138
  config_text = config_path.read_text(encoding="utf-8")
139
+ config_enabled = _replace_many(
140
+ config_text,
141
+ {
142
+ "__prj_dir__": str(project_root),
143
+ "__app_name__": str(app_name),
144
+ "__app_dir__": str(app_root),
145
+ "__g_address__": options.address,
146
+ "__g_port__": str(options.port),
147
+ "__g_workers__": str(options.workers),
148
+ "__g_reload__": "True" if options.reload else "False",
149
+ },
150
+ )
151
+ config_path.write_text(config_enabled, encoding="utf-8")
152
+
153
+ # gunicorn.conf.py
154
+ gunicorn_conf_path = app_root / "gunicorn.conf.py"
155
+ gunicorn_conf_text = gunicorn_conf_path.read_text(encoding="utf-8")
156
+ gunicorn_conf_enabled = _replace_many(gunicorn_conf_text, {"__app_name__": app_name})
157
+ gunicorn_conf_path.write_text(gunicorn_conf_enabled, encoding="utf-8")
158
+
159
+ # wsgi.py
160
+ wsgi_path = app_root / "wsgi.py"
161
+ wsgi_text = wsgi_path.read_text(encoding="utf-8")
162
+ wsgi_enabled = _replace_many(wsgi_text, {"__app_name__": app_name})
163
+ wsgi_path.write_text(wsgi_enabled, encoding="utf-8")
164
+
165
+ # pages/_templates.py
166
+ pages_templates_path = pages_dir / "_templates.py"
167
+ pages_templates_text = pages_templates_path.read_text(encoding="utf-8")
168
+ pages_templates_enabled = _replace_many(pages_templates_text, {"__app_name__": app_name})
169
+ pages_templates_path.write_text(pages_templates_enabled, encoding="utf-8")
170
+
171
+ # gateway config (optional; only if gateway/ exists OR gateway_config explicitly given)
172
+ if gateway_config:
173
+ gw = project_root / "gateway"
174
+ if not gw.exists() or not gw.is_dir():
175
+ # ここは勝手に作らない(start-projectの責務に寄せる)
176
+ raise AppBuildError("gateway/ does not exist. Run start-project --gateway ... first.")
177
+ _write_gateway_config(
178
+ gw,
179
+ kind=gateway_config,
180
+ app_name=app_name,
181
+ app_port=options.port,
182
+ domain=domain,
183
+ )
184
+
185
+ return app_root
186
+
88
187
 
89
- # NOTE: pwd_dir should be the generated project directory, not current cwd.
90
- enabled = config_text.format(
91
- pwd_dir=str(out_dir),
92
- g_address=options.address,
93
- g_port=options.port,
94
- g_workers=options.workers,
95
- g_reload=options.reload,
188
+ def create_auth(
189
+ project_dir: str | Path,
190
+ *,
191
+ options: AppBuildOptions | None = None,
192
+ ) -> Path:
193
+ options = options or AppBuildOptions(port=7070)
194
+
195
+ project_root = Path(project_dir).expanduser().resolve()
196
+ project_root.mkdir(parents=True, exist_ok=True)
197
+
198
+ app_root = project_root / "auth"
199
+ if app_root.exists():
200
+ raise FileExistsError(f"Target auth app already exists: {app_root}")
201
+ app_root.mkdir(parents=True, exist_ok=False)
202
+
203
+ app_pkg = "nexom.assets.auth"
204
+ for fn in ("__init__.py", "gunicorn.conf.py", "wsgi.py", "config.py"):
205
+ _copy_from_package(app_pkg, fn, app_root / fn)
206
+
207
+ config_path = app_root / "config.py"
208
+ config_text = config_path.read_text(encoding="utf-8")
209
+ config_enabled = _replace_many(
210
+ config_text,
211
+ {
212
+ "__prj_dir__": str(project_root),
213
+ "__app_name__": "auth",
214
+ "__app_dir__": str(app_root),
215
+ "__g_address__": options.address,
216
+ "__g_port__": str(options.port),
217
+ "__g_workers__": str(options.workers),
218
+ "__g_reload__": "True" if options.reload else "False",
219
+ },
220
+ )
221
+ config_path.write_text(config_enabled, encoding="utf-8")
222
+
223
+ gunicorn_conf_path = app_root / "gunicorn.conf.py"
224
+ gunicorn_conf_text = gunicorn_conf_path.read_text(encoding="utf-8")
225
+ gunicorn_conf_enabled = _replace_many(gunicorn_conf_text, {"__app_name__": "auth"})
226
+ gunicorn_conf_path.write_text(gunicorn_conf_enabled, encoding="utf-8")
227
+
228
+ return app_root
229
+
230
+
231
+ def start_project(
232
+ *,
233
+ project_root: Path,
234
+ main_name: str = "app",
235
+ auth_name: str = "auth",
236
+ main_options: AppBuildOptions | None = None,
237
+ auth_options: AppBuildOptions | None = None,
238
+ gateway: str = "none", # none|nginx|apache
239
+ domain: str = "",
240
+ ) -> Path:
241
+ """
242
+ Assumption: user already created the project directory and cd'ed into it.
243
+ So we DO NOT create the project directory itself; we only populate inside.
244
+ """
245
+ _validate_app_name(main_name)
246
+ _validate_app_name(auth_name)
247
+
248
+ root = project_root.expanduser().resolve()
249
+ if not root.exists() or not root.is_dir():
250
+ raise AppBuildError("Project root must be an existing directory.")
251
+
252
+ # data/
253
+ data_dir = root / "data"
254
+ log_dir = data_dir / "log"
255
+ db_dir = data_dir / "db"
256
+ data_dir.mkdir(parents=True, exist_ok=True)
257
+ log_dir.mkdir(parents=True, exist_ok=True)
258
+ db_dir.mkdir(parents=True, exist_ok=True)
259
+
260
+ # main app + auth app
261
+ main_opt = main_options or AppBuildOptions()
262
+ auth_opt = auth_options or AppBuildOptions(port=7070)
263
+
264
+ # auth is usually fixed folder name "auth", but you asked folder name selectable.
265
+ # create_auth creates "auth" fixed, so we generate via create_app then remove extras?
266
+ # ここは仕様を守って assets/app の auth用テンプレを “auth_name” に生成する。
267
+ # => create_app を使ってから不要物を消す、よりは “auth専用生成” を auth_name 対応にする。
268
+ # なので start_project 内で auth側を “auth_name” に生成する専用処理を持つ。
269
+
270
+ # main
271
+ create_app(root, main_name, options=main_opt, gateway_config=None)
272
+
273
+ # auth (auth_name)
274
+ auth_root = root / auth_name
275
+ if auth_root.exists():
276
+ raise FileExistsError(f"Target app already exists: {auth_root}")
277
+ auth_root.mkdir(parents=True, exist_ok=False)
278
+
279
+ app_pkg = "nexom.assets.auth"
280
+ for fn in ("__init__.py", "gunicorn.conf.py", "wsgi.py", "config.py"):
281
+ _copy_from_package(app_pkg, fn, auth_root / fn)
282
+
283
+ # config.py
284
+ config_path = auth_root / "config.py"
285
+ config_text = config_path.read_text(encoding="utf-8")
286
+ config_enabled = _replace_many(
287
+ config_text,
288
+ {
289
+ "__prj_dir__": str(root),
290
+ "__app_name__": "auth",
291
+ "__app_dir__": str(auth_root),
292
+ "__g_address__": auth_opt.address,
293
+ "__g_port__": str(auth_opt.port),
294
+ "__g_workers__": str(auth_opt.workers),
295
+ "__g_reload__": "True" if auth_opt.reload else "False",
296
+ },
96
297
  )
97
- config_path.write_text(enabled, encoding="utf-8")
298
+ config_path.write_text(config_enabled, encoding="utf-8")
299
+
300
+ # gunicorn.conf.py
301
+ gunicorn_conf_path = auth_root / "gunicorn.conf.py"
302
+ gunicorn_conf_text = gunicorn_conf_path.read_text(encoding="utf-8")
303
+ gunicorn_conf_enabled = _replace_many(gunicorn_conf_text, {"__app_name__": auth_name})
304
+ gunicorn_conf_path.write_text(gunicorn_conf_enabled, encoding="utf-8")
305
+
306
+ # gateway/
307
+ if gateway != "none":
308
+ gw = root / "gateway"
309
+ gw.mkdir(parents=True, exist_ok=True)
310
+
311
+ _write_gateway_config(
312
+ gw,
313
+ kind=gateway,
314
+ app_name=main_name,
315
+ app_port=main_opt.port,
316
+ domain=domain,
317
+ )
98
318
 
99
- return out_dir
319
+ return root
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import signal
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Iterable
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class DetectedApp:
17
+ name: str
18
+ dir: Path
19
+ wsgi: Path
20
+ gunicorn_conf: Path
21
+
22
+
23
+ class RunError(RuntimeError):
24
+ """Raised when naxom run fails."""
25
+
26
+
27
+ def _is_windows() -> bool:
28
+ return os.name == "nt" or platform.system().lower() == "windows"
29
+
30
+
31
+ def _detect_apps(project_root: Path) -> list[DetectedApp]:
32
+ root = project_root.resolve()
33
+ if not root.exists() or not root.is_dir():
34
+ raise RunError("Project root is not a directory.")
35
+
36
+ apps: list[DetectedApp] = []
37
+ for p in root.iterdir():
38
+ if not p.is_dir():
39
+ continue
40
+ wsgi = p / "wsgi.py"
41
+ conf = p / "gunicorn.conf.py"
42
+ if wsgi.exists() and conf.exists():
43
+ apps.append(
44
+ DetectedApp(
45
+ name=p.name,
46
+ dir=p,
47
+ wsgi=wsgi,
48
+ gunicorn_conf=conf,
49
+ )
50
+ )
51
+ return sorted(apps, key=lambda a: a.name)
52
+
53
+
54
+ def _require_gunicorn() -> str:
55
+ gunicorn = shutil.which("gunicorn")
56
+ if not gunicorn:
57
+ raise RunError("gunicorn not found in PATH. Install it (pip install gunicorn).")
58
+ return gunicorn
59
+
60
+
61
+ def _build_cmd(gunicorn_bin: str, app: DetectedApp) -> list[str]:
62
+ # project root を PYTHONPATH に入れて、"appdir.wsgi:app" が import できる前提
63
+ target = f"{app.name}.wsgi:app"
64
+ return [gunicorn_bin, target, "--config", str(app.gunicorn_conf)]
65
+
66
+
67
+ def run_project(
68
+ project_root: Path,
69
+ app_names: list[str] | None = None,
70
+ *,
71
+ dry_run: bool = False,
72
+ ) -> None:
73
+ """
74
+ Run WSGI apps in a Nexom project directory.
75
+
76
+ - Detect apps: directories that contain both wsgi.py and gunicorn.conf.py.
77
+ - If app_names is empty -> run all detected apps.
78
+ - Else -> run only the specified ones.
79
+
80
+ Notes:
81
+ - Uses subprocess to start gunicorn (stable + predictable).
82
+ - On Windows: gunicorn isn't supported (POSIX only) -> raise clear error.
83
+ """
84
+ if _is_windows():
85
+ raise RunError("naxom run is not supported on Windows (gunicorn is POSIX-only).")
86
+
87
+ root = project_root.resolve()
88
+ apps = _detect_apps(root)
89
+ if not apps:
90
+ raise RunError("No runnable apps found. (Need <app>/wsgi.py and <app>/gunicorn.conf.py)")
91
+
92
+ app_map = {a.name: a for a in apps}
93
+
94
+ selected: list[DetectedApp]
95
+ if not app_names:
96
+ selected = apps
97
+ else:
98
+ missing = [n for n in app_names if n not in app_map]
99
+ if missing:
100
+ raise RunError(f"App not found: {', '.join(missing)}")
101
+ selected = [app_map[n] for n in app_names]
102
+
103
+ gunicorn = _require_gunicorn()
104
+
105
+ # 重要: project root を import 解決に使う
106
+ env = os.environ.copy()
107
+ env["PYTHONPATH"] = str(root) + (os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else "")
108
+
109
+ procs: list[subprocess.Popen[bytes]] = []
110
+ cmds: list[tuple[str, list[str]]] = []
111
+
112
+ for app in selected:
113
+ cmd = _build_cmd(gunicorn, app)
114
+ cmds.append((app.name, cmd))
115
+
116
+ if dry_run:
117
+ for name, cmd in cmds:
118
+ print(f"[dry-run] {name}: {' '.join(cmd)}")
119
+ return
120
+
121
+ # 起動
122
+ for name, cmd in cmds:
123
+ print(f"[run] starting {name}")
124
+ # 新しいプロセスグループにして、まとめて止めやすくする(POSIX)
125
+ p = subprocess.Popen(
126
+ cmd,
127
+ cwd=str(root),
128
+ env=env,
129
+ stdout=sys.stdout.buffer,
130
+ stderr=sys.stderr.buffer,
131
+ start_new_session=True,
132
+ )
133
+ procs.append(p)
134
+
135
+ def _terminate_all(sig: int) -> None:
136
+ # まず優しく
137
+ for p in procs:
138
+ if p.poll() is None:
139
+ try:
140
+ os.killpg(p.pid, signal.SIGTERM)
141
+ except Exception:
142
+ try:
143
+ p.terminate()
144
+ except Exception:
145
+ pass
146
+
147
+ # 少し待つ
148
+ deadline = time.time() + 5.0
149
+ while time.time() < deadline:
150
+ if all(p.poll() is not None for p in procs):
151
+ return
152
+ time.sleep(0.05)
153
+
154
+ # 最後は強制
155
+ for p in procs:
156
+ if p.poll() is None:
157
+ try:
158
+ os.killpg(p.pid, signal.SIGKILL)
159
+ except Exception:
160
+ try:
161
+ p.kill()
162
+ except Exception:
163
+ pass
164
+
165
+ # 親が落ちる/止まる時にまとめて止める
166
+ def _handle_signal(_signum: int, _frame) -> None:
167
+ _terminate_all(_signum)
168
+ raise KeyboardInterrupt
169
+
170
+ signal.signal(signal.SIGINT, _handle_signal)
171
+ signal.signal(signal.SIGTERM, _handle_signal)
172
+
173
+ # どれか1つ死んだら全体止める(中途半端が一番事故る)
174
+ try:
175
+ while True:
176
+ for p in procs:
177
+ code = p.poll()
178
+ if code is not None:
179
+ raise RunError(f"Process exited (pid={p.pid}, code={code})")
180
+ time.sleep(0.2)
181
+ except KeyboardInterrupt:
182
+ _terminate_all(signal.SIGINT)
183
+ except Exception:
184
+ _terminate_all(signal.SIGTERM)
185
+ raise
nexom/core/__init__.py CHANGED
@@ -1 +1,2 @@
1
- from . import error
1
+ from . import error
2
+ from . import object_html_render
nexom/core/error.py CHANGED
@@ -128,7 +128,7 @@ class ObjectHTMLInsertValueError(NexomError):
128
128
 
129
129
  def __init__(self, name: str) -> None:
130
130
  super().__init__(
131
- "OH01",
131
+ "OH02",
132
132
  f"This insert value is invalid. '{name}'",
133
133
  )
134
134
  class ObjectHTMLExtendsError(NexomError):
@@ -136,7 +136,7 @@ class ObjectHTMLExtendsError(NexomError):
136
136
 
137
137
  def __init__(self, name: str) -> None:
138
138
  super().__init__(
139
- "OH02",
139
+ "OH03",
140
140
  f"This extends is invalid. '{name}'",
141
141
  )
142
142
  class ObjectHTMLImportError(NexomError):
@@ -144,6 +144,84 @@ class ObjectHTMLImportError(NexomError):
144
144
 
145
145
  def __init__(self, name: str) -> None:
146
146
  super().__init__(
147
- "OH03",
147
+ "OH04",
148
148
  f"This import is invalid. '{name}'",
149
+ )
150
+ class ObjectHTMLTypeError(NexomError):
151
+ """Raised when an set HTMLDoc for type is valid."""
152
+
153
+ def __init__(self) -> None:
154
+ super().__init__(
155
+ "OH05",
156
+ f"This doc is not HTMLDoc'",
157
+ )
158
+
159
+
160
+ # =========================
161
+ # DatabaseManager
162
+ # =========================
163
+ class DBMConnectionInvalidError(NexomError):
164
+ """Raised when an udbm connection is invalid."""
165
+
166
+ def __init__(self, message="Not started") -> None:
167
+ super().__init__(
168
+ "DBM01",
169
+ f"DBM connection is invalid. -> {message}",
170
+ )
171
+
172
+ class DBError(NexomError):
173
+ """Raised when an udbm connection is invalid."""
174
+
175
+ def __init__(self, message) -> None:
176
+ super().__init__(
177
+ "DBM02",
178
+ f"DBM connection is invalid. -> {message}",
179
+ )
180
+
181
+ # =========================
182
+ # Auth
183
+ # =========================
184
+
185
+ class AuthMissingFieldError(NexomError):
186
+ """Raised when required auth fields are missing."""
187
+ def __init__(self, key: str) -> None:
188
+ super().__init__(
189
+ "A01",
190
+ f"Missing field. '{key}'"
191
+ )
192
+
193
+
194
+ class AuthInvalidCredentialsError(NexomError):
195
+ """Raised when user_id or password is invalid."""
196
+ def __init__(self) -> None:
197
+ super().__init__(
198
+ "A02",
199
+ "Invalid credentials."
200
+ )
201
+
202
+
203
+ class AuthUserDisabledError(NexomError):
204
+ """Raised when the user is inactive/disabled."""
205
+ def __init__(self) -> None:
206
+ super().__init__(
207
+ "A03",
208
+ "This user is disabled."
209
+ )
210
+
211
+
212
+ class AuthTokenInvalidError(NexomError):
213
+ """Raised when token is missing/invalid/expired/revoked."""
214
+ def __init__(self) -> None:
215
+ super().__init__(
216
+ "A04",
217
+ "This token is invalid."
218
+ )
219
+
220
+
221
+ class AuthServiceUnavailableError(NexomError):
222
+ """Raised when AuthService is unreachable or failed to respond."""
223
+ def __init__(self) -> None:
224
+ super().__init__(
225
+ "A05",
226
+ "Authentication service is currently unavailable."
149
227
  )