cave-cli 3.6.0b2__tar.gz → 3.6.0b4__tar.gz
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.
- {cave_cli-3.6.0b2/cave_cli.egg-info → cave_cli-3.6.0b4}/PKG-INFO +1 -1
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/display.py +108 -27
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4/cave_cli.egg-info}/PKG-INFO +1 -1
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli.egg-info/SOURCES.txt +1 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/pyproject.toml +1 -1
- cave_cli-3.6.0b4/test/test_display.py +64 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/LICENSE +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/NOTICE.md +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/README.md +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/__init__.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/cli.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/__init__.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/create.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/doctor.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/kill.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/list_cmd.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/list_versions.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/prettify.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/purge.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/reset.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/run.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/sync_cmd.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/test.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/theme.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/uninstall.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/update.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/upgrade.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/commands/version.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/__init__.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/cache.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/constants.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/docker.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/env.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/git.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/logger.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/net.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/subprocess.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/sync.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/validate.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli/utils/version.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli.egg-info/dependency_links.txt +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli.egg-info/entry_points.txt +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli.egg-info/requires.txt +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/cave_cli.egg-info/top_level.txt +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/setup.cfg +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/test/test_env.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/test/test_net.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/test/test_sync.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/test/test_upgrade.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/test/test_validate.py +0 -0
- {cave_cli-3.6.0b2 → cave_cli-3.6.0b4}/utils/prettify.py +0 -0
|
@@ -178,11 +178,19 @@ class LogLine:
|
|
|
178
178
|
# ── Log filtering ──────────────────────────────────────────────────────────
|
|
179
179
|
|
|
180
180
|
_LEVEL_PREFIX_RE = re.compile(
|
|
181
|
-
r"^(INFO|WARNING|WARN|ERROR|DEBUG|CRITICAL):[ \t]
|
|
181
|
+
r"^(INFO|WARNING|WARN|ERROR|DEBUG|CRITICAL):[ \t]*", re.IGNORECASE
|
|
182
182
|
)
|
|
183
183
|
|
|
184
|
+
# Uvicorn's own formatter prepends a level prefix to every message (e.g.
|
|
185
|
+
# "INFO: Started server process [1]"), and the container's run script
|
|
186
|
+
# wraps that again with its own level prefix, producing nested prefixes such
|
|
187
|
+
# as "INFO: INFO: ...". strip_level_prefix() below strips these
|
|
188
|
+
# iteratively so a fully-clean message is left either way.
|
|
189
|
+
_ACCESS_LOG_RE = re.compile(r'"[A-Z]+ \S* HTTP/\d\.\d"')
|
|
190
|
+
|
|
184
191
|
# Lines to silently drop in clean mode (very noisy, no user value).
|
|
185
|
-
# WebSocket CONNECT/DISCONNECT
|
|
192
|
+
# WebSocket CONNECT/DISCONNECT (old Channels runserver) and SOCKET CONNECTION
|
|
193
|
+
# OPENED/CLOSED (custom markers) are processed for client count but not displayed.
|
|
186
194
|
_SKIP_PATTERNS: tuple[str, ...] = (
|
|
187
195
|
"WS RECEIVE ",
|
|
188
196
|
"HTTP ",
|
|
@@ -192,11 +200,34 @@ _SKIP_PATTERNS: tuple[str, ...] = (
|
|
|
192
200
|
"Django version ",
|
|
193
201
|
"Starting ASGI",
|
|
194
202
|
"Starting development server",
|
|
195
|
-
"Quit the server with CONTROL-C",
|
|
203
|
+
"Quit the server with CONTROL-C",
|
|
196
204
|
"Watching for file changes",
|
|
197
205
|
"Performing system checks",
|
|
198
206
|
"System check identified no issues",
|
|
199
207
|
"changed, reloading",
|
|
208
|
+
|
|
209
|
+
# Uvicorn Startup Lines
|
|
210
|
+
"Started server process ",
|
|
211
|
+
"Waiting for application startup",
|
|
212
|
+
"Waiting for application shutdown",
|
|
213
|
+
"Application startup complete",
|
|
214
|
+
"Application shutdown complete",
|
|
215
|
+
"Uvicorn running on",
|
|
216
|
+
"Shutting down",
|
|
217
|
+
"Finished server process ",
|
|
218
|
+
"detected changes in",
|
|
219
|
+
"connection open",
|
|
220
|
+
"connection closed",
|
|
221
|
+
"Will watch for changes in these directories",
|
|
222
|
+
"Started reloader process",
|
|
223
|
+
"StatReload",
|
|
224
|
+
"Reloading",
|
|
225
|
+
"reloading",
|
|
226
|
+
|
|
227
|
+
# Custom socket markers (processed for client count, not displayed)
|
|
228
|
+
"SOCKET CONNECTION OPENED",
|
|
229
|
+
"SOCKET CONNECTION CLOSED",
|
|
230
|
+
|
|
200
231
|
# Current Date. EG: April 28, 2026
|
|
201
232
|
f"{time.strftime('%B %d, %Y')}",
|
|
202
233
|
)
|
|
@@ -215,12 +246,34 @@ _VALIDATION_PATTERNS: tuple[str, ...] = (
|
|
|
215
246
|
"RuntimeWarning",
|
|
216
247
|
)
|
|
217
248
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
249
|
+
# Django (old) and Uvicorn (new) status transition triggers, checked against
|
|
250
|
+
# the fully level-prefix-stripped line. Uvicorn only prints its startup
|
|
251
|
+
# banner ("Uvicorn running on ...") once per process, not on every autoreload
|
|
252
|
+
# cycle, so "Application startup complete" -- which does reprint each
|
|
253
|
+
# reload -- is used as the READY trigger instead.
|
|
254
|
+
_LOADING_TRIGGERS = (
|
|
255
|
+
"Starting ASGI",
|
|
256
|
+
"Starting development server",
|
|
257
|
+
"Started server process ",
|
|
258
|
+
"Waiting for application startup",
|
|
259
|
+
)
|
|
260
|
+
_READY_TRIGGERS = (
|
|
261
|
+
"Quit the server with CONTROL-C",
|
|
262
|
+
"Application startup complete",
|
|
263
|
+
"Ready",
|
|
264
|
+
"App Ready",
|
|
265
|
+
)
|
|
266
|
+
_RELOAD_TRIGGERS = (
|
|
267
|
+
"changed, reloading",
|
|
268
|
+
"detected changes in",
|
|
269
|
+
"Reloading",
|
|
270
|
+
"reloading",
|
|
271
|
+
)
|
|
221
272
|
|
|
222
273
|
_WS_CONNECT = "WebSocket CONNECT "
|
|
223
274
|
_WS_DISCONNECT = "WebSocket DISCONNECT "
|
|
275
|
+
_SOCKET_OPENED = "SOCKET CONNECTION OPENED"
|
|
276
|
+
_SOCKET_CLOSED = "SOCKET CONNECTION CLOSED"
|
|
224
277
|
|
|
225
278
|
|
|
226
279
|
class LogFilter:
|
|
@@ -253,8 +306,19 @@ class LogFilter:
|
|
|
253
306
|
- ``stripped``:
|
|
254
307
|
- Type: str
|
|
255
308
|
- What: The line with the level prefix removed.
|
|
309
|
+
|
|
310
|
+
Notes:
|
|
311
|
+
|
|
312
|
+
- Strips repeatedly so nested prefixes (e.g. a container run
|
|
313
|
+
script's "INFO: " wrapped around uvicorn's own "INFO: "
|
|
314
|
+
formatter output) are fully removed, not just the outermost one.
|
|
256
315
|
"""
|
|
257
|
-
|
|
316
|
+
stripped = line
|
|
317
|
+
while True:
|
|
318
|
+
new = _LEVEL_PREFIX_RE.sub("", stripped, count=1)
|
|
319
|
+
if new == stripped:
|
|
320
|
+
return new
|
|
321
|
+
stripped = new
|
|
258
322
|
|
|
259
323
|
@staticmethod
|
|
260
324
|
def classify_status(stripped: str) -> str | None:
|
|
@@ -279,12 +343,12 @@ class LogFilter:
|
|
|
279
343
|
|
|
280
344
|
- READY is checked first so a reload cycle correctly resets the status.
|
|
281
345
|
"""
|
|
282
|
-
if
|
|
346
|
+
if any(t in stripped for t in _READY_TRIGGERS):
|
|
283
347
|
return READY
|
|
284
|
-
if
|
|
285
|
-
return LOADING
|
|
286
|
-
if _RELOAD_TRIGGER in stripped:
|
|
348
|
+
if any(t in stripped for t in _RELOAD_TRIGGERS):
|
|
287
349
|
return RELOADING
|
|
350
|
+
if any(t in stripped for t in _LOADING_TRIGGERS):
|
|
351
|
+
return LOADING
|
|
288
352
|
return None
|
|
289
353
|
|
|
290
354
|
@staticmethod
|
|
@@ -305,10 +369,16 @@ class LogFilter:
|
|
|
305
369
|
- ``event``:
|
|
306
370
|
- Type: str | None
|
|
307
371
|
- What: ``"connect"`` or ``"disconnect"``, or None if not a WS event.
|
|
372
|
+
|
|
373
|
+
Notes:
|
|
374
|
+
|
|
375
|
+
- Recognizes both the old Django Channels runserver format
|
|
376
|
+
("WebSocket CONNECT "/"WebSocket DISCONNECT ") and the custom
|
|
377
|
+
uvicorn app markers ("SOCKET CONNECTION OPENED"/"CLOSED").
|
|
308
378
|
"""
|
|
309
|
-
if _WS_CONNECT in stripped:
|
|
379
|
+
if _WS_CONNECT in stripped or _SOCKET_OPENED in stripped:
|
|
310
380
|
return "connect"
|
|
311
|
-
if _WS_DISCONNECT in stripped:
|
|
381
|
+
if _WS_DISCONNECT in stripped or _SOCKET_CLOSED in stripped:
|
|
312
382
|
return "disconnect"
|
|
313
383
|
return None
|
|
314
384
|
|
|
@@ -327,6 +397,8 @@ class LogFilter:
|
|
|
327
397
|
"""
|
|
328
398
|
if not stripped.strip():
|
|
329
399
|
return True
|
|
400
|
+
if _ACCESS_LOG_RE.search(stripped):
|
|
401
|
+
return True
|
|
330
402
|
return any(p in stripped for p in _SKIP_PATTERNS)
|
|
331
403
|
|
|
332
404
|
@staticmethod
|
|
@@ -350,7 +422,12 @@ class LogFilter:
|
|
|
350
422
|
|
|
351
423
|
- Raw-line starts with WARNING:/ERROR: are always validation.
|
|
352
424
|
- Indented lines are treated as traceback continuation frames.
|
|
425
|
+
- Uvicorn's autoreload notice ("WatchFiles detected changes in
|
|
426
|
+
...") is logged at WARNING level but is a benign status message,
|
|
427
|
+
not an error, so reload triggers are excluded first.
|
|
353
428
|
"""
|
|
429
|
+
if any(t in stripped for t in _RELOAD_TRIGGERS):
|
|
430
|
+
return False
|
|
354
431
|
raw_upper = raw.upper()
|
|
355
432
|
if raw_upper.startswith("WARNING:") or raw_upper.startswith("ERROR:"):
|
|
356
433
|
return True
|
|
@@ -715,20 +792,24 @@ class RunDashboard:
|
|
|
715
792
|
# Status transitions (always checked)
|
|
716
793
|
new_status = self._filter.classify_status(stripped)
|
|
717
794
|
if new_status:
|
|
718
|
-
|
|
719
|
-
if new_status ==
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
795
|
+
# During reload, do not transition status back to LOADING when the new worker process starts
|
|
796
|
+
if self._status == RELOADING and new_status == LOADING:
|
|
797
|
+
pass
|
|
798
|
+
else:
|
|
799
|
+
self._status = new_status
|
|
800
|
+
if new_status == RELOADING:
|
|
801
|
+
self._ws_clients = 0
|
|
802
|
+
reloading_entry = LogLine(timestamp=ts, text="App Reloading", raw="INFO: App Reloading")
|
|
803
|
+
add_minimal(reloading_entry)
|
|
804
|
+
add_all(reloading_entry)
|
|
805
|
+
if self._show_all and self._scroll_offset > 0:
|
|
806
|
+
self._scroll_offset += 1
|
|
807
|
+
elif new_status == READY:
|
|
808
|
+
ready_entry = LogLine(timestamp=ts, text="App Ready", raw="INFO: App Ready")
|
|
809
|
+
add_minimal(ready_entry)
|
|
810
|
+
add_all(ready_entry)
|
|
811
|
+
if self._show_all and self._scroll_offset > 0:
|
|
812
|
+
self._scroll_offset += 1
|
|
732
813
|
|
|
733
814
|
# WebSocket client count (before noise filter so we never miss events)
|
|
734
815
|
ws_ev = self._filter.ws_event(stripped)
|
|
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
|
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
14
|
name = "cave_cli"
|
|
15
|
-
version = "3.6.
|
|
15
|
+
version = "3.6.0b4"
|
|
16
16
|
description = "CLI for creating and managing Docker-based CAVE web applications."
|
|
17
17
|
authors = [
|
|
18
18
|
{name = "MIT-CAVE", email = "cave-contact@mit.edu"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from cave_cli.utils.display import LogFilter, RunDashboard, READY, LOADING, RELOADING
|
|
3
|
+
|
|
4
|
+
class TestLogFilter:
|
|
5
|
+
def test_strip_level_prefix(self):
|
|
6
|
+
filter_obj = LogFilter()
|
|
7
|
+
assert filter_obj.strip_level_prefix("INFO: Started server process [134449]") == "Started server process [134449]"
|
|
8
|
+
assert filter_obj.strip_level_prefix("INFO: INFO: Started server process [134449]") == "Started server process [134449]"
|
|
9
|
+
assert filter_obj.strip_level_prefix("WARNING: Connection lost") == "Connection lost"
|
|
10
|
+
assert filter_obj.strip_level_prefix("connection open") == "connection open"
|
|
11
|
+
|
|
12
|
+
def test_ws_event(self):
|
|
13
|
+
filter_obj = LogFilter()
|
|
14
|
+
# Old Channels
|
|
15
|
+
assert filter_obj.ws_event("WebSocket CONNECT /ws/") == "connect"
|
|
16
|
+
assert filter_obj.ws_event("WebSocket DISCONNECT /ws/") == "disconnect"
|
|
17
|
+
|
|
18
|
+
# Custom markers
|
|
19
|
+
assert filter_obj.ws_event("SOCKET CONNECTION OPENED") == "connect"
|
|
20
|
+
assert filter_obj.ws_event("SOCKET CONNECTION CLOSED") == "disconnect"
|
|
21
|
+
|
|
22
|
+
# New Uvicorn vanilla markers should not be classified as events to avoid double counting
|
|
23
|
+
assert filter_obj.ws_event("connection open") is None
|
|
24
|
+
assert filter_obj.ws_event("connection closed") is None
|
|
25
|
+
|
|
26
|
+
# Other logs
|
|
27
|
+
assert filter_obj.ws_event("some other message") is None
|
|
28
|
+
|
|
29
|
+
def test_classify_status(self):
|
|
30
|
+
filter_obj = LogFilter()
|
|
31
|
+
assert filter_obj.classify_status("Application startup complete.") == READY
|
|
32
|
+
assert filter_obj.classify_status("Started server process [134449]") == LOADING
|
|
33
|
+
assert filter_obj.classify_status("detected changes in 'app.py', reloading") == RELOADING
|
|
34
|
+
assert filter_obj.classify_status("StatReload detected file change in 'app.py' - Reloading...") == RELOADING
|
|
35
|
+
assert filter_obj.classify_status("Reloading...") == RELOADING
|
|
36
|
+
assert filter_obj.classify_status("Just some logging") is None
|
|
37
|
+
|
|
38
|
+
def test_is_noise(self):
|
|
39
|
+
filter_obj = LogFilter()
|
|
40
|
+
assert filter_obj.is_noise("connection open") is True
|
|
41
|
+
assert filter_obj.is_noise("connection closed") is True
|
|
42
|
+
assert filter_obj.is_noise("SOCKET CONNECTION OPENED") is True
|
|
43
|
+
assert filter_obj.is_noise("WebSocket CONNECT /ws/") is True
|
|
44
|
+
assert filter_obj.is_noise("Started server process ") is True
|
|
45
|
+
assert filter_obj.is_noise("Will watch for changes in these directories: ['/app']") is True
|
|
46
|
+
assert filter_obj.is_noise("Started reloader process [48] using StatReload") is True
|
|
47
|
+
assert filter_obj.is_noise("some actual log line") is False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestRunDashboard:
|
|
51
|
+
def test_reload_log_entry(self):
|
|
52
|
+
dashboard = RunDashboard(app_name="test_app", url="http://localhost:8000")
|
|
53
|
+
assert len(dashboard._log_lines) == 1
|
|
54
|
+
assert dashboard._log_lines[0].text == "App Loading"
|
|
55
|
+
|
|
56
|
+
dashboard._process_line("INFO: Application startup complete.")
|
|
57
|
+
assert dashboard._log_lines[-1].text == "App Ready"
|
|
58
|
+
|
|
59
|
+
dashboard._process_line("WARNING: StatReload detected file change in 'app.py' - Reloading...")
|
|
60
|
+
assert dashboard._log_lines[-1].text == "App Reloading"
|
|
61
|
+
assert dashboard._log_lines[-1].raw == "INFO: App Reloading"
|
|
62
|
+
|
|
63
|
+
dashboard._process_line("INFO: Application startup complete.")
|
|
64
|
+
assert dashboard._log_lines[-1].text == "App Ready"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|