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.
- nexom/__init__.py +2 -2
- nexom/__main__.py +111 -17
- nexom/app/__init__.py +62 -0
- nexom/app/auth.py +322 -0
- nexom/{web → app}/cookie.py +4 -2
- nexom/app/db.py +88 -0
- nexom/app/path.py +195 -0
- nexom/app/request.py +267 -0
- nexom/{web → app}/response.py +13 -3
- nexom/{web → app}/template.py +1 -1
- nexom/app/user.py +31 -0
- nexom/assets/app/__init__.py +0 -0
- nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom/assets/app/config.py +28 -0
- nexom/assets/app/gunicorn.conf.py +5 -0
- nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom/assets/app/pages/_templates.py +7 -0
- nexom/assets/{server → app}/pages/default.py +2 -2
- nexom/assets/{server → app}/pages/document.py +2 -2
- nexom/assets/app/router.py +12 -0
- nexom/assets/app/wsgi.py +64 -0
- nexom/assets/auth/__init__.py +0 -0
- nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom/assets/auth/config.py +27 -0
- nexom/assets/auth/gunicorn.conf.py +5 -0
- nexom/assets/auth/wsgi.py +62 -0
- nexom/assets/auth_page/login.html +95 -0
- nexom/assets/auth_page/signup.html +106 -0
- nexom/assets/error_page/error.html +3 -3
- nexom/assets/gateway/apache_app.conf +16 -0
- nexom/assets/gateway/nginx_app.conf +21 -0
- nexom/buildTools/__init__.py +1 -0
- nexom/buildTools/build.py +274 -54
- nexom/buildTools/run.py +185 -0
- nexom/core/__init__.py +2 -1
- nexom/core/error.py +81 -3
- nexom/core/log.py +111 -0
- nexom/{engine → core}/object_html_render.py +4 -1
- nexom/templates/__init__.py +0 -0
- nexom/templates/auth.py +72 -0
- {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/METADATA +75 -50
- nexom-1.0.1.dist-info/RECORD +56 -0
- {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/WHEEL +1 -1
- nexom/assets/server/config.py +0 -27
- nexom/assets/server/gunicorn.conf.py +0 -16
- nexom/assets/server/pages/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom/assets/server/pages/_templates.py +0 -11
- nexom/assets/server/router.py +0 -18
- nexom/assets/server/wsgi.py +0 -30
- nexom/engine/__init__.py +0 -1
- nexom/web/__init__.py +0 -5
- nexom/web/path.py +0 -125
- nexom/web/request.py +0 -62
- nexom-0.1.4.dist-info/RECORD +0 -39
- /nexom/{web → app}/http_status_codes.py +0 -0
- /nexom/{web → app}/middleware.py +0 -0
- /nexom/assets/{server → app}/pages/__init__.py +0 -0
- /nexom/assets/{server → app}/static/dog.jpeg +0 -0
- /nexom/assets/{server → app}/static/style.css +0 -0
- /nexom/assets/{server → app}/templates/base.html +0 -0
- /nexom/assets/{server → app}/templates/default.html +0 -0
- /nexom/assets/{server → app}/templates/document.html +0 -0
- /nexom/assets/{server → app}/templates/footer.html +0 -0
- /nexom/assets/{server → app}/templates/header.html +0 -0
- {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/entry_points.txt +0 -0
- {nexom-0.1.4.dist-info → nexom-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
static_dir = out_dir / "static"
|
|
64
|
+
# domain placeholder
|
|
65
|
+
dom = domain.strip() or "YOUR_DOMAIN_HERE # set --domain example.com"
|
|
55
66
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
#
|
|
66
|
-
pages_pkg = "nexom.assets.
|
|
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
|
-
#
|
|
71
|
-
templates_pkg = "nexom.assets.
|
|
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
|
-
#
|
|
76
|
-
static_pkg = "nexom.assets.
|
|
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
|
-
#
|
|
81
|
-
app_pkg = "nexom.assets.
|
|
82
|
-
for fn in ("gunicorn.conf.py", "router.py", "wsgi.py", "config.py"):
|
|
83
|
-
_copy_from_package(app_pkg, 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
|
-
#
|
|
86
|
-
config_path =
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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
|
|
319
|
+
return root
|
nexom/buildTools/run.py
ADDED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
)
|