cave-cli 3.6.0b2__tar.gz → 3.6.0b3__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.
Files changed (51) hide show
  1. {cave_cli-3.6.0b2/cave_cli.egg-info → cave_cli-3.6.0b3}/PKG-INFO +1 -1
  2. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/display.py +102 -26
  3. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3/cave_cli.egg-info}/PKG-INFO +1 -1
  4. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli.egg-info/SOURCES.txt +1 -0
  5. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/pyproject.toml +1 -1
  6. cave_cli-3.6.0b3/test/test_display.py +46 -0
  7. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/LICENSE +0 -0
  8. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/NOTICE.md +0 -0
  9. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/README.md +0 -0
  10. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/__init__.py +0 -0
  11. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/cli.py +0 -0
  12. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/__init__.py +0 -0
  13. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/create.py +0 -0
  14. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/doctor.py +0 -0
  15. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/kill.py +0 -0
  16. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/list_cmd.py +0 -0
  17. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/list_versions.py +0 -0
  18. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/prettify.py +0 -0
  19. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/purge.py +0 -0
  20. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/reset.py +0 -0
  21. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/run.py +0 -0
  22. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/sync_cmd.py +0 -0
  23. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/test.py +0 -0
  24. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/theme.py +0 -0
  25. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/uninstall.py +0 -0
  26. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/update.py +0 -0
  27. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/upgrade.py +0 -0
  28. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/commands/version.py +0 -0
  29. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/__init__.py +0 -0
  30. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/cache.py +0 -0
  31. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/constants.py +0 -0
  32. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/docker.py +0 -0
  33. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/env.py +0 -0
  34. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/git.py +0 -0
  35. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/logger.py +0 -0
  36. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/net.py +0 -0
  37. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/subprocess.py +0 -0
  38. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/sync.py +0 -0
  39. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/validate.py +0 -0
  40. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli/utils/version.py +0 -0
  41. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli.egg-info/dependency_links.txt +0 -0
  42. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli.egg-info/entry_points.txt +0 -0
  43. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli.egg-info/requires.txt +0 -0
  44. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/cave_cli.egg-info/top_level.txt +0 -0
  45. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/setup.cfg +0 -0
  46. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/test/test_env.py +0 -0
  47. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/test/test_net.py +0 -0
  48. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/test/test_sync.py +0 -0
  49. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/test/test_upgrade.py +0 -0
  50. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/test/test_validate.py +0 -0
  51. {cave_cli-3.6.0b2 → cave_cli-3.6.0b3}/utils/prettify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cave_cli
3
- Version: 3.6.0b2
3
+ Version: 3.6.0b3
4
4
  Summary: CLI for creating and managing Docker-based CAVE web applications.
5
5
  Author-email: MIT-CAVE <cave-contact@mit.edu>
6
6
  License: Apache-2.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]?", re.IGNORECASE
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 are processed for client count but not displayed.
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,31 @@ _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
+
224
+ # Custom socket markers (processed for client count, not displayed)
225
+ "SOCKET CONNECTION OPENED",
226
+ "SOCKET CONNECTION CLOSED",
227
+
200
228
  # Current Date. EG: April 28, 2026
201
229
  f"{time.strftime('%B %d, %Y')}",
202
230
  )
@@ -215,12 +243,32 @@ _VALIDATION_PATTERNS: tuple[str, ...] = (
215
243
  "RuntimeWarning",
216
244
  )
217
245
 
218
- _LOADING_TRIGGER = "Starting ASGI"
219
- _READY_TRIGGER = "Quit the server with CONTROL-C"
220
- _RELOAD_TRIGGER = "changed, reloading"
246
+ # Django (old) and Uvicorn (new) status transition triggers, checked against
247
+ # the fully level-prefix-stripped line. Uvicorn only prints its startup
248
+ # banner ("Uvicorn running on ...") once per process, not on every autoreload
249
+ # cycle, so "Application startup complete" -- which does reprint each
250
+ # reload -- is used as the READY trigger instead.
251
+ _LOADING_TRIGGERS = (
252
+ "Starting ASGI",
253
+ "Starting development server",
254
+ "Started server process ",
255
+ "Waiting for application startup",
256
+ "Reloading",
257
+ )
258
+ _READY_TRIGGERS = (
259
+ "Quit the server with CONTROL-C",
260
+ "Application startup complete",
261
+ "Ready"
262
+ )
263
+ _RELOAD_TRIGGERS = (
264
+ "changed, reloading",
265
+ "detected changes in",
266
+ )
221
267
 
222
268
  _WS_CONNECT = "WebSocket CONNECT "
223
269
  _WS_DISCONNECT = "WebSocket DISCONNECT "
270
+ _SOCKET_OPENED = "SOCKET CONNECTION OPENED"
271
+ _SOCKET_CLOSED = "SOCKET CONNECTION CLOSED"
224
272
 
225
273
 
226
274
  class LogFilter:
@@ -253,8 +301,19 @@ class LogFilter:
253
301
  - ``stripped``:
254
302
  - Type: str
255
303
  - What: The line with the level prefix removed.
304
+
305
+ Notes:
306
+
307
+ - Strips repeatedly so nested prefixes (e.g. a container run
308
+ script's "INFO: " wrapped around uvicorn's own "INFO: "
309
+ formatter output) are fully removed, not just the outermost one.
256
310
  """
257
- return _LEVEL_PREFIX_RE.sub("", line, count=1)
311
+ stripped = line
312
+ while True:
313
+ new = _LEVEL_PREFIX_RE.sub("", stripped, count=1)
314
+ if new == stripped:
315
+ return new
316
+ stripped = new
258
317
 
259
318
  @staticmethod
260
319
  def classify_status(stripped: str) -> str | None:
@@ -279,11 +338,11 @@ class LogFilter:
279
338
 
280
339
  - READY is checked first so a reload cycle correctly resets the status.
281
340
  """
282
- if _READY_TRIGGER in stripped:
341
+ if any(t in stripped for t in _READY_TRIGGERS):
283
342
  return READY
284
- if _LOADING_TRIGGER in stripped or "Starting development server" in stripped:
343
+ if any(t in stripped for t in _LOADING_TRIGGERS):
285
344
  return LOADING
286
- if _RELOAD_TRIGGER in stripped:
345
+ if any(t in stripped for t in _RELOAD_TRIGGERS):
287
346
  return RELOADING
288
347
  return None
289
348
 
@@ -305,10 +364,16 @@ class LogFilter:
305
364
  - ``event``:
306
365
  - Type: str | None
307
366
  - What: ``"connect"`` or ``"disconnect"``, or None if not a WS event.
367
+
368
+ Notes:
369
+
370
+ - Recognizes both the old Django Channels runserver format
371
+ ("WebSocket CONNECT "/"WebSocket DISCONNECT ") and the custom
372
+ uvicorn app markers ("SOCKET CONNECTION OPENED"/"CLOSED").
308
373
  """
309
- if _WS_CONNECT in stripped:
374
+ if _WS_CONNECT in stripped or _SOCKET_OPENED in stripped:
310
375
  return "connect"
311
- if _WS_DISCONNECT in stripped:
376
+ if _WS_DISCONNECT in stripped or _SOCKET_CLOSED in stripped:
312
377
  return "disconnect"
313
378
  return None
314
379
 
@@ -327,6 +392,8 @@ class LogFilter:
327
392
  """
328
393
  if not stripped.strip():
329
394
  return True
395
+ if _ACCESS_LOG_RE.search(stripped):
396
+ return True
330
397
  return any(p in stripped for p in _SKIP_PATTERNS)
331
398
 
332
399
  @staticmethod
@@ -350,7 +417,12 @@ class LogFilter:
350
417
 
351
418
  - Raw-line starts with WARNING:/ERROR: are always validation.
352
419
  - Indented lines are treated as traceback continuation frames.
420
+ - Uvicorn's autoreload notice ("WatchFiles detected changes in
421
+ ...") is logged at WARNING level but is a benign status message,
422
+ not an error, so reload triggers are excluded first.
353
423
  """
424
+ if any(t in stripped for t in _RELOAD_TRIGGERS):
425
+ return False
354
426
  raw_upper = raw.upper()
355
427
  if raw_upper.startswith("WARNING:") or raw_upper.startswith("ERROR:"):
356
428
  return True
@@ -715,20 +787,24 @@ class RunDashboard:
715
787
  # Status transitions (always checked)
716
788
  new_status = self._filter.classify_status(stripped)
717
789
  if new_status:
718
- self._status = new_status
719
- if new_status == RELOADING:
720
- self._ws_clients = 0
721
- reloading_entry = LogLine(timestamp=ts, text="Reloading", raw=f"INFO: {stripped}")
722
- add_minimal(reloading_entry)
723
- add_all(reloading_entry)
724
- if self._show_all and self._scroll_offset > 0:
725
- self._scroll_offset += 1
726
- elif new_status == READY:
727
- ready_entry = LogLine(timestamp=ts, text="Ready", raw="INFO: Ready")
728
- add_minimal(ready_entry)
729
- add_all(ready_entry)
730
- if self._show_all and self._scroll_offset > 0:
731
- self._scroll_offset += 1
790
+ # During reload, do not transition status back to LOADING when the new worker process starts
791
+ if self._status == RELOADING and new_status == LOADING:
792
+ pass
793
+ else:
794
+ self._status = new_status
795
+ if new_status == RELOADING:
796
+ self._ws_clients = 0
797
+ reloading_entry = LogLine(timestamp=ts, text="Reloading", raw=f"INFO: {stripped}")
798
+ add_minimal(reloading_entry)
799
+ add_all(reloading_entry)
800
+ if self._show_all and self._scroll_offset > 0:
801
+ self._scroll_offset += 1
802
+ elif new_status == READY:
803
+ ready_entry = LogLine(timestamp=ts, text="Ready", raw="INFO: Ready")
804
+ add_minimal(ready_entry)
805
+ add_all(ready_entry)
806
+ if self._show_all and self._scroll_offset > 0:
807
+ self._scroll_offset += 1
732
808
 
733
809
  # WebSocket client count (before noise filter so we never miss events)
734
810
  ws_ev = self._filter.ws_event(stripped)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cave_cli
3
- Version: 3.6.0b2
3
+ Version: 3.6.0b3
4
4
  Summary: CLI for creating and managing Docker-based CAVE web applications.
5
5
  Author-email: MIT-CAVE <cave-contact@mit.edu>
6
6
  License: Apache-2.0
@@ -40,6 +40,7 @@ cave_cli/utils/subprocess.py
40
40
  cave_cli/utils/sync.py
41
41
  cave_cli/utils/validate.py
42
42
  cave_cli/utils/version.py
43
+ test/test_display.py
43
44
  test/test_env.py
44
45
  test/test_net.py
45
46
  test/test_sync.py
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "cave_cli"
15
- version = "3.6.0b2"
15
+ version = "3.6.0b3"
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,46 @@
1
+ import pytest
2
+ from cave_cli.utils.display import LogFilter, 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("Just some logging") is None
35
+
36
+ def test_is_noise(self):
37
+ filter_obj = LogFilter()
38
+ assert filter_obj.is_noise("connection open") is True
39
+ assert filter_obj.is_noise("connection closed") is True
40
+ assert filter_obj.is_noise("SOCKET CONNECTION OPENED") is True
41
+ assert filter_obj.is_noise("WebSocket CONNECT /ws/") is True
42
+ assert filter_obj.is_noise("Started server process ") is True
43
+ assert filter_obj.is_noise("Will watch for changes in these directories: ['/app']") is True
44
+ assert filter_obj.is_noise("Started reloader process [48] using StatReload") is True
45
+ assert filter_obj.is_noise("some actual log line") is False
46
+
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