create-caspian-app 0.2.0-beta.49 → 0.2.0-beta.50
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.
- package/dist/main.py +63 -29
- package/dist/settings/bs-config.ts +5 -5
- package/dist/settings/python-server.ts +22 -7
- package/package.json +1 -1
package/dist/main.py
CHANGED
|
@@ -6,6 +6,7 @@ import importlib.util
|
|
|
6
6
|
import mimetypes
|
|
7
7
|
import secrets
|
|
8
8
|
import traceback
|
|
9
|
+
import json
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from fastapi import FastAPI, Request, Response
|
|
11
12
|
from fastapi.responses import RedirectResponse, FileResponse, HTMLResponse
|
|
@@ -35,7 +36,6 @@ from casp.layout import (
|
|
|
35
36
|
import hashlib
|
|
36
37
|
from casp.streaming import SSE
|
|
37
38
|
from typing import Any, Optional, get_args, get_origin, Union
|
|
38
|
-
import re
|
|
39
39
|
from src.lib.auth.auth_config import build_auth_settings
|
|
40
40
|
|
|
41
41
|
load_dotenv()
|
|
@@ -70,6 +70,37 @@ IS_PRODUCTION = os.getenv('APP_ENV') == 'production'
|
|
|
70
70
|
CACHE_ENABLED = os.getenv('CACHE_ENABLED', 'false').lower() == 'true'
|
|
71
71
|
DEFAULT_TTL = int(os.getenv('CACHE_TTL', 600))
|
|
72
72
|
|
|
73
|
+
|
|
74
|
+
def _dev_cookie_scope() -> str:
|
|
75
|
+
if IS_PRODUCTION:
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
scope = os.getenv("CASPIAN_BROWSER_SYNC_PORT")
|
|
79
|
+
if not scope:
|
|
80
|
+
bs_config_path = Path("settings/bs-config.json")
|
|
81
|
+
if bs_config_path.exists():
|
|
82
|
+
try:
|
|
83
|
+
local_url = json.loads(
|
|
84
|
+
bs_config_path.read_text(encoding="utf-8")
|
|
85
|
+
).get("local", "")
|
|
86
|
+
scope = local_url.rsplit(":", 1)[-1].strip("/")
|
|
87
|
+
except (OSError, json.JSONDecodeError):
|
|
88
|
+
scope = ""
|
|
89
|
+
|
|
90
|
+
scope = scope or os.getenv("PORT", "")
|
|
91
|
+
return scope if scope.isdigit() else ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _scoped_cookie_name(base_name: str) -> str:
|
|
95
|
+
scope = _dev_cookie_scope()
|
|
96
|
+
return f"{base_name}_{scope}" if scope else base_name
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
CSRF_COOKIE_NAME = _scoped_cookie_name("pp_csrf")
|
|
100
|
+
SESSION_COOKIE_NAME = _scoped_cookie_name(
|
|
101
|
+
os.getenv('AUTH_COOKIE_NAME', 'session')
|
|
102
|
+
)
|
|
103
|
+
|
|
73
104
|
# ====
|
|
74
105
|
# Static File Routes
|
|
75
106
|
# ====
|
|
@@ -129,7 +160,7 @@ class CSRFMiddleware:
|
|
|
129
160
|
|
|
130
161
|
async def send_wrapper(message):
|
|
131
162
|
if message["type"] == "http.response.start":
|
|
132
|
-
cookie_value = f"
|
|
163
|
+
cookie_value = f"{CSRF_COOKIE_NAME}={csrf_token}; Path=/; SameSite=Lax"
|
|
133
164
|
if IS_PRODUCTION:
|
|
134
165
|
cookie_value += "; Secure"
|
|
135
166
|
new_headers = list(message.get("headers", []))
|
|
@@ -159,7 +190,7 @@ class AuthMiddleware:
|
|
|
159
190
|
providers = Auth.get_providers()
|
|
160
191
|
|
|
161
192
|
if providers:
|
|
162
|
-
oauth_response = auth_inst.auth_providers(*providers)
|
|
193
|
+
oauth_response = await auth_inst.auth_providers(*providers)
|
|
163
194
|
if oauth_response:
|
|
164
195
|
await oauth_response(scope, receive, send)
|
|
165
196
|
return
|
|
@@ -217,40 +248,44 @@ class RPCMiddleware:
|
|
|
217
248
|
# Route Registration
|
|
218
249
|
# ====
|
|
219
250
|
|
|
251
|
+
_route_module_cache = {}
|
|
252
|
+
_route_signature_cache = {}
|
|
220
253
|
|
|
221
|
-
_VOID_TAGS_PATTERN = r"(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)"
|
|
222
|
-
_VOID_END_TAG_RE = re.compile(rf"</\s*{_VOID_TAGS_PATTERN}\s*>", re.IGNORECASE)
|
|
223
|
-
_VOID_OPEN_TAG_RE = re.compile(
|
|
224
|
-
rf"<\s*({_VOID_TAGS_PATTERN})(\b[^>]*)>", re.IGNORECASE)
|
|
225
254
|
|
|
255
|
+
def load_route_module(file_path: str):
|
|
256
|
+
abs_path = os.path.abspath(file_path)
|
|
257
|
+
try:
|
|
258
|
+
mtime_ns = os.stat(abs_path).st_mtime_ns
|
|
259
|
+
except OSError:
|
|
260
|
+
raise FileNotFoundError(f"Route module not found: {abs_path}")
|
|
226
261
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _open_repl(m: re.Match) -> str:
|
|
231
|
-
tag = m.group(1)
|
|
232
|
-
attrs = m.group(2) or ""
|
|
233
|
-
full = m.group(0)
|
|
234
|
-
if full.rstrip().endswith("/>"):
|
|
235
|
-
return full
|
|
236
|
-
if attrs and not attrs.startswith(" "):
|
|
237
|
-
attrs = " " + attrs
|
|
238
|
-
return f"<{tag}{attrs} />"
|
|
239
|
-
|
|
240
|
-
return _VOID_OPEN_TAG_RE.sub(_open_repl, html)
|
|
241
|
-
|
|
262
|
+
cached = _route_module_cache.get(abs_path)
|
|
263
|
+
if cached is not None and cached[0] == mtime_ns:
|
|
264
|
+
return cached[1]
|
|
242
265
|
|
|
243
|
-
|
|
244
|
-
unique_id = hashlib.md5(file_path.encode()).hexdigest()[:8]
|
|
266
|
+
unique_id = hashlib.md5(abs_path.encode()).hexdigest()[:8]
|
|
245
267
|
module_name = f"page_{unique_id}"
|
|
246
|
-
spec = importlib.util.spec_from_file_location(module_name,
|
|
268
|
+
spec = importlib.util.spec_from_file_location(module_name, abs_path)
|
|
247
269
|
assert spec is not None and spec.loader is not None, f"Cannot load spec for {file_path}"
|
|
248
270
|
module = importlib.util.module_from_spec(spec)
|
|
249
271
|
spec.loader.exec_module(module)
|
|
250
272
|
setattr(module, 'render_page', render_page)
|
|
273
|
+
_route_module_cache[abs_path] = (mtime_ns, module)
|
|
274
|
+
_route_signature_cache.pop(abs_path, None)
|
|
251
275
|
return module
|
|
252
276
|
|
|
253
277
|
|
|
278
|
+
def get_page_signature(file_path: str, page_func):
|
|
279
|
+
abs_path = os.path.abspath(file_path)
|
|
280
|
+
cached = _route_signature_cache.get(abs_path)
|
|
281
|
+
if cached is not None and cached[0] is page_func:
|
|
282
|
+
return cached[1]
|
|
283
|
+
|
|
284
|
+
sig = inspect.signature(page_func)
|
|
285
|
+
_route_signature_cache[abs_path] = (page_func, sig)
|
|
286
|
+
return sig
|
|
287
|
+
|
|
288
|
+
|
|
254
289
|
def _unwrap_optional(annotation: Any) -> Any:
|
|
255
290
|
"""
|
|
256
291
|
Optional[T] is Union[T, NoneType]. Return T when applicable.
|
|
@@ -338,7 +373,7 @@ def register_single_route(url_pattern: str, file_path: str):
|
|
|
338
373
|
current_uri = request.url.path
|
|
339
374
|
|
|
340
375
|
# 1. Cache Check (Fast Path)
|
|
341
|
-
if request.method == 'GET':
|
|
376
|
+
if CACHE_ENABLED and request.method == 'GET':
|
|
342
377
|
cached_resp = CacheHandler.serve_cache(current_uri, DEFAULT_TTL)
|
|
343
378
|
if cached_resp:
|
|
344
379
|
return HTMLResponse(content=cached_resp)
|
|
@@ -356,7 +391,7 @@ def register_single_route(url_pattern: str, file_path: str):
|
|
|
356
391
|
if not hasattr(module, 'page'):
|
|
357
392
|
raise AttributeError(f"Missing 'def page():' in {file_path}")
|
|
358
393
|
|
|
359
|
-
sig =
|
|
394
|
+
sig = get_page_signature(file_path, module.page)
|
|
360
395
|
call_kwargs = {}
|
|
361
396
|
call_args = []
|
|
362
397
|
|
|
@@ -434,7 +469,6 @@ def register_single_route(url_pattern: str, file_path: str):
|
|
|
434
469
|
)
|
|
435
470
|
|
|
436
471
|
html_output = transform_scripts(html_output)
|
|
437
|
-
html_output = normalize_void_tags(html_output)
|
|
438
472
|
response = HTMLResponse(content=html_output)
|
|
439
473
|
response.headers['X-PP-Root-Layout'] = root_layout_id
|
|
440
474
|
|
|
@@ -540,7 +574,7 @@ app.add_middleware(CSRFMiddleware)
|
|
|
540
574
|
app.add_middleware(
|
|
541
575
|
SessionMiddleware,
|
|
542
576
|
secret_key=os.getenv('AUTH_SECRET', 'change-me'),
|
|
543
|
-
session_cookie=
|
|
577
|
+
session_cookie=SESSION_COOKIE_NAME,
|
|
544
578
|
max_age=SESSION_LIFETIME_HOURS * 3600,
|
|
545
579
|
same_site='lax',
|
|
546
580
|
https_only=IS_PRODUCTION,
|
|
@@ -165,7 +165,7 @@ const pipeline = new DebouncedWorker(
|
|
|
165
165
|
!changedFile.includes("__pycache__");
|
|
166
166
|
|
|
167
167
|
if (needsPythonRestart) {
|
|
168
|
-
await restartPythonServer(pythonPort);
|
|
168
|
+
await restartPythonServer(pythonPort, bsPort);
|
|
169
169
|
updateRouteFilesCache();
|
|
170
170
|
|
|
171
171
|
const isReady = await waitForPort(pythonPort);
|
|
@@ -201,7 +201,7 @@ const pipeline = new DebouncedWorker(
|
|
|
201
201
|
"→ Structure changed (New/Deleted file), restarting Python server...",
|
|
202
202
|
),
|
|
203
203
|
);
|
|
204
|
-
await restartPythonServer(pythonPort);
|
|
204
|
+
await restartPythonServer(pythonPort, bsPort);
|
|
205
205
|
const isReady = await waitForPort(pythonPort);
|
|
206
206
|
if (isReady && bs.active) bs.reload();
|
|
207
207
|
} else if (bs.active) {
|
|
@@ -295,7 +295,7 @@ const publicPipeline = new DebouncedWorker(
|
|
|
295
295
|
createSrcWatcher(join(__dirname, "..", "utils", "**", "*.py"), {
|
|
296
296
|
onEvent: async (_ev, _abs, _) => {
|
|
297
297
|
if (_abs.includes("__pycache__")) return;
|
|
298
|
-
await restartPythonServer(pythonPort);
|
|
298
|
+
await restartPythonServer(pythonPort, bsPort);
|
|
299
299
|
const isReady = await waitForPort(pythonPort);
|
|
300
300
|
if (isReady && bs.active) bs.reload();
|
|
301
301
|
},
|
|
@@ -308,7 +308,7 @@ const publicPipeline = new DebouncedWorker(
|
|
|
308
308
|
createSrcWatcher(join(__dirname, "..", "main.py"), {
|
|
309
309
|
onEvent: async (_ev, _abs, _) => {
|
|
310
310
|
if (_abs.includes("__pycache__")) return;
|
|
311
|
-
await restartPythonServer(pythonPort);
|
|
311
|
+
await restartPythonServer(pythonPort, bsPort);
|
|
312
312
|
const isReady = await waitForPort(pythonPort);
|
|
313
313
|
if (isReady && bs.active) bs.reload();
|
|
314
314
|
},
|
|
@@ -318,7 +318,7 @@ const publicPipeline = new DebouncedWorker(
|
|
|
318
318
|
interval: 1000,
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
startPythonServer(pythonPort);
|
|
321
|
+
startPythonServer(pythonPort, bsPort);
|
|
322
322
|
|
|
323
323
|
bs.init(
|
|
324
324
|
{
|
|
@@ -53,7 +53,7 @@ export function waitForPort(port: number, timeout = 10000): Promise<boolean> {
|
|
|
53
53
|
|
|
54
54
|
export function waitForPortRelease(
|
|
55
55
|
port: number,
|
|
56
|
-
timeout = 5000
|
|
56
|
+
timeout = 5000,
|
|
57
57
|
): Promise<boolean> {
|
|
58
58
|
const start = Date.now();
|
|
59
59
|
return new Promise((resolve) => {
|
|
@@ -114,29 +114,44 @@ async function killProcessTree(child: ChildProcess): Promise<void> {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function spawnPython(port: number): ChildProcess {
|
|
117
|
+
function spawnPython(port: number, browserSyncPort?: number): ChildProcess {
|
|
118
118
|
const pythonPath = getVenvPythonPath();
|
|
119
119
|
const args = ["-u", "main.py"];
|
|
120
120
|
|
|
121
121
|
console.log(`→ Starting Python server on port ${port}...`);
|
|
122
122
|
|
|
123
|
+
const env = {
|
|
124
|
+
...process.env,
|
|
125
|
+
PYTHONUNBUFFERED: "1",
|
|
126
|
+
PORT: String(port),
|
|
127
|
+
...(browserSyncPort
|
|
128
|
+
? { CASPIAN_BROWSER_SYNC_PORT: String(browserSyncPort) }
|
|
129
|
+
: {}),
|
|
130
|
+
};
|
|
131
|
+
|
|
123
132
|
const child = spawn(pythonPath, args, {
|
|
124
133
|
stdio: "inherit",
|
|
125
134
|
shell: false,
|
|
126
135
|
detached: !isWindows(),
|
|
127
|
-
env
|
|
136
|
+
env,
|
|
128
137
|
});
|
|
129
138
|
|
|
130
139
|
child.on("error", (err) => console.error("Failed to start Python:", err));
|
|
131
140
|
return child;
|
|
132
141
|
}
|
|
133
142
|
|
|
134
|
-
export function startPythonServer(
|
|
143
|
+
export function startPythonServer(
|
|
144
|
+
port: number,
|
|
145
|
+
browserSyncPort?: number,
|
|
146
|
+
): void {
|
|
135
147
|
if (pythonProcess && pythonProcess.exitCode === null) return;
|
|
136
|
-
pythonProcess = spawnPython(port);
|
|
148
|
+
pythonProcess = spawnPython(port, browserSyncPort);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
export async function restartPythonServer(
|
|
151
|
+
export async function restartPythonServer(
|
|
152
|
+
port: number,
|
|
153
|
+
browserSyncPort?: number,
|
|
154
|
+
): Promise<void> {
|
|
140
155
|
if (isRestarting) return;
|
|
141
156
|
isRestarting = true;
|
|
142
157
|
|
|
@@ -150,7 +165,7 @@ export async function restartPythonServer(port: number): Promise<void> {
|
|
|
150
165
|
await waitForPortRelease(port);
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
pythonProcess = spawnPython(port);
|
|
168
|
+
pythonProcess = spawnPython(port, browserSyncPort);
|
|
154
169
|
} finally {
|
|
155
170
|
isRestarting = false;
|
|
156
171
|
}
|