ms-components 0.5.0__tar.gz → 0.5.2__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 (140) hide show
  1. {ms_components-0.5.0 → ms_components-0.5.2}/PKG-INFO +1 -1
  2. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_executors.py +4 -2
  3. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/detail.py +19 -1
  4. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/models.py +1 -0
  5. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/polling.py +1 -0
  6. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/resources.py +12 -3
  7. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components.egg-info/PKG-INFO +1 -1
  8. ms_components-0.5.2/src/ms_components.egg-info/scm_version.json +8 -0
  9. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_molsuite_monitor_widgets.py +26 -0
  10. ms_components-0.5.0/src/ms_components.egg-info/scm_version.json +0 -8
  11. {ms_components-0.5.0 → ms_components-0.5.2}/.github/workflows/docs.yml +0 -0
  12. {ms_components-0.5.0 → ms_components-0.5.2}/.github/workflows/publish.yml +0 -0
  13. {ms_components-0.5.0 → ms_components-0.5.2}/.gitignore +0 -0
  14. {ms_components-0.5.0 → ms_components-0.5.2}/LICENSE +0 -0
  15. {ms_components-0.5.0 → ms_components-0.5.2}/MANIFEST.in +0 -0
  16. {ms_components-0.5.0 → ms_components-0.5.2}/README.md +0 -0
  17. {ms_components-0.5.0 → ms_components-0.5.2}/examples/tutorial_demo.py +0 -0
  18. {ms_components-0.5.0 → ms_components-0.5.2}/examples/tutorial_demo.toml +0 -0
  19. {ms_components-0.5.0 → ms_components-0.5.2}/licenses/qt-themes-LICENSE.txt +0 -0
  20. {ms_components-0.5.0 → ms_components-0.5.2}/pyproject.toml +0 -0
  21. {ms_components-0.5.0 → ms_components-0.5.2}/setup.cfg +0 -0
  22. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/__init__.py +0 -0
  23. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/backdrop.py +0 -0
  24. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/base.qss +0 -0
  25. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/chips.py +0 -0
  26. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ketcher.py +0 -0
  27. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/listview.py +0 -0
  28. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_dockwidget/__init__.py +0 -0
  29. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_dockwidget/resources/__init__.py +0 -0
  30. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_dockwidget/resources/icons/__init__.py +0 -0
  31. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_dockwidget/resources/icons/folder.svg +0 -0
  32. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_dockwidget/widget.py +0 -0
  33. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/__init__.py +0 -0
  34. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/bridge.py +0 -0
  35. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/config.py +0 -0
  36. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/formatting.py +0 -0
  37. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/jobs.py +0 -0
  38. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/palette.py +0 -0
  39. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_monitor/widget.py +0 -0
  40. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_projects/__init__.py +0 -0
  41. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_projects/app.py +0 -0
  42. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_projects/backend.py +0 -0
  43. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_projects/browser.py +0 -0
  44. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_projects/scratch.py +0 -0
  45. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_projects/widget.py +0 -0
  46. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/__init__.py +0 -0
  47. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/command_line.py +0 -0
  48. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/actions.svg +0 -0
  49. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/box.svg +0 -0
  50. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/center.svg +0 -0
  51. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/display.svg +0 -0
  52. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/hide.svg +0 -0
  53. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/hydrogens-polar.svg +0 -0
  54. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/hydrogens.svg +0 -0
  55. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/menu.svg +0 -0
  56. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/orient.svg +0 -0
  57. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/presets.svg +0 -0
  58. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/rock.svg +0 -0
  59. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/selection.svg +0 -0
  60. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/settings.svg +0 -0
  61. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/style.svg +0 -0
  62. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/icons/zoom.svg +0 -0
  63. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/plugins.py +0 -0
  64. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/scene.py +0 -0
  65. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_pymol/widget.py +0 -0
  66. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_settings/__init__.py +0 -0
  67. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_settings/panel.py +0 -0
  68. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_stepper.py +0 -0
  69. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/__init__.py +0 -0
  70. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/action_header.py +0 -0
  71. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/cell_delegate.py +0 -0
  72. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/column_panel.py +0 -0
  73. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/example_usage.py +0 -0
  74. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/filter_panel.py +0 -0
  75. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/columns.svg +0 -0
  76. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/export.svg +0 -0
  77. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/filter.svg +0 -0
  78. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/menu.svg +0 -0
  79. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/reload.svg +0 -0
  80. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/search.svg +0 -0
  81. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/settings.svg +0 -0
  82. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/sort-asc.svg +0 -0
  83. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/sort-desc.svg +0 -0
  84. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/icons/sort.svg +0 -0
  85. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/metric_filter_bar.py +0 -0
  86. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/pagination_control.py +0 -0
  87. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/query_builder.py +0 -0
  88. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/session_provider.py +0 -0
  89. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/smart_table_view.py +0 -0
  90. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/sort_panel.py +0 -0
  91. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/table_config.py +0 -0
  92. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/table_interaction.py +0 -0
  93. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/table_model.py +0 -0
  94. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/test_external_filters.py +0 -0
  95. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_table/test_toolbar.py +0 -0
  96. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_tutorial/__init__.py +0 -0
  97. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_tutorial/bindings.py +0 -0
  98. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_tutorial/controller.py +0 -0
  99. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_tutorial/models.py +0 -0
  100. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/ms_tutorial/overlay.py +0 -0
  101. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/opengl.py +0 -0
  102. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/pagination.py +0 -0
  103. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/palettes.py +0 -0
  104. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/step_dialog.py +0 -0
  105. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/theme.py +0 -0
  106. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/tool_panel.py +0 -0
  107. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components/wheel.py +0 -0
  108. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components.egg-info/SOURCES.txt +0 -0
  109. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components.egg-info/dependency_links.txt +0 -0
  110. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components.egg-info/requires.txt +0 -0
  111. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components.egg-info/scm_file_list.json +0 -0
  112. {ms_components-0.5.0 → ms_components-0.5.2}/src/ms_components.egg-info/top_level.txt +0 -0
  113. {ms_components-0.5.0 → ms_components-0.5.2}/tests/conftest.py +0 -0
  114. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_app_settings.py +0 -0
  115. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_chips.py +0 -0
  116. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_dock_window.py +0 -0
  117. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_executors_ui.py +0 -0
  118. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_ketcher.py +0 -0
  119. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_molsuite_monitor_bridge.py +0 -0
  120. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_monitor_config.py +0 -0
  121. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_ms_projects_widget.py +0 -0
  122. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_opengl.py +0 -0
  123. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_optional_integrations.py +0 -0
  124. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_progress_column.py +0 -0
  125. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_project_browser.py +0 -0
  126. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_pymol_command_line.py +0 -0
  127. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_pymol_grid.py +0 -0
  128. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_pymol_scene.py +0 -0
  129. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_pymol_toolbar.py +0 -0
  130. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_query_builder_keyset.py +0 -0
  131. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_shift_hscroll.py +0 -0
  132. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_split_button_style.py +0 -0
  133. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_step_dialog.py +0 -0
  134. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_table_interaction.py +0 -0
  135. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_table_toolbar.py +0 -0
  136. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_table_view_state.py +0 -0
  137. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_theme_contrast.py +0 -0
  138. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_tool_panel.py +0 -0
  139. {ms_components-0.5.0 → ms_components-0.5.2}/tests/test_tutorial.py +0 -0
  140. {ms_components-0.5.0 → ms_components-0.5.2}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ms_components
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: PySide6 UI components for MolSuite: smart table, project browser, job monitor, PyMOL dock, steppers.
5
5
  Author: Mario S. Valdés-Tresanco, Mario E. Valdés-Tresanco
6
6
  License-Expression: MIT
@@ -165,9 +165,11 @@ def describe_host(address: str, env: str = "", packages=(), tools=None) -> str:
165
165
  tools = tools or {}
166
166
  python = f"{env.rstrip('/')}/bin/python" if env else ("" if address else sys.executable)
167
167
  script = "; ".join([
168
- "export LC_ALL=C", "uname -srm", '(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME")',
168
+ "export LC_ALL=C", "uname -srm",
169
+ '(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME") || (v=$(sw_vers -productVersion 2>/dev/null) && echo "macOS $v")',
169
170
  'echo "CPUs: $(nproc 2>/dev/null || sysctl -n hw.ncpu)"',
170
- "awk '/MemTotal/ {printf \"Memory: %.1f GB\\n\", $2/1048576}' /proc/meminfo 2>/dev/null",
171
+ "awk '/MemTotal/ {printf \"Memory: %.1f GB\\n\", $2/1048576}' /proc/meminfo 2>/dev/null"
172
+ " || sysctl -n hw.memsize | awk '{printf \"Memory: %.1f GB\\n\", $1/1073741824}'", # macOS: no /proc
171
173
  "(nvidia-smi -L 2>/dev/null || echo 'GPUs: none seen by nvidia-smi')",
172
174
  "echo",
173
175
  *([f"if [ -x {shlex.quote(python)} ]; then {shlex.quote(python)} -c {shlex.quote(_VERSIONS)} "
@@ -628,7 +628,7 @@ class JobDetail(QWidget):
628
628
  self._traceback.setPlainText("" if item is None else str(item.data(Qt.UserRole) or ""))
629
629
 
630
630
  _FACTS = ("key", "status", "executor", "started", "finished", "elapsed", "chunks", "retried", "throughput",
631
- "peak memory", "in flight", "errors")
631
+ "peak memory", "CPU", "GPU", "in flight", "errors")
632
632
 
633
633
  def _build_summary(self) -> None:
634
634
  self._fact: dict[str, QLabel] = {}
@@ -661,6 +661,7 @@ class JobDetail(QWidget):
661
661
  f"{job.chunks_retried:,}",
662
662
  format_speed(job.throughput_eps) + ("" if job.is_terminal else " · last 5 min"),
663
663
  f"{job.peak_mem_mb:,} MB" if job.peak_mem_mb else "—",
664
+ *_used(job, (end - job.started_at).total_seconds() if job.started_at else 0.0),
664
665
  f"{job.tasks_running:,} tasks · {job.cpu_used:,} CPUs" if not job.is_terminal else "—",
665
666
  f"{self._error_types:,} types" if self._error_types else "none",
666
667
  )
@@ -668,4 +669,21 @@ class JobDetail(QWidget):
668
669
  self._fact[key].setText(value)
669
670
 
670
671
 
672
+ def _used(job: JobMonitorState, seconds: float) -> tuple[str, str]:
673
+ """The CPU and GPU lines of a job's summary: what it kept busy, of what it held."""
674
+ usage = job.usage
675
+ if not usage or seconds <= 0:
676
+ return "—", "—"
677
+ cpu = gpu = "—"
678
+ if usage.get("peak_cpu"):
679
+ held = f"{usage['cpu_s'] / seconds:,.1f} held on average · {usage['peak_cpu']:g} at most"
680
+ busy = usage.get("busy_s") # on this machine only: what it read while the job ran
681
+ cpu = held if busy is None else f"{busy / 3600:,.2f} CPU·h · {busy / seconds:,.1f} busy of " + held
682
+ if usage.get("peak_gpu"):
683
+ gpu = f"{usage['gpu_s'] / 3600:,.2f} GPU·h · {usage['peak_gpu']:g} at most"
684
+ if usage.get("vram_mb") is not None:
685
+ gpu += f" · {usage['vram_mb']:,} MB VRAM at most · {usage['gpu_util']:.0f} % busy on average"
686
+ return cpu, gpu
687
+
688
+
671
689
  __all__ = ["ChunkGrid", "GRID_MAX", "GRID_STEP", "JobDetail", "counts", "grid_window", "layout_cells", "reason"]
@@ -50,6 +50,7 @@ class JobMonitorState:
50
50
  cpu_used: int = 0 # the CPU tokens those tasks hold
51
51
  chunks_retried: int = 0
52
52
  peak_mem_mb: int = 0
53
+ usage: dict = field(default_factory=dict) # what it used, kept once it ended: see ms_flow's JobSnapshot.usage
53
54
  throughput_eps: float = 0.0 # chunks settled per second over the last minutes; since it began once it ended
54
55
  throughput_history: list[float] = field(default_factory=list) # the same, one value per few seconds
55
56
  started_at: datetime | None = None
@@ -33,6 +33,7 @@ def job_state(job, now: float) -> JobMonitorState:
33
33
  cpu_used=getattr(job, "cpu_used", 0),
34
34
  chunks_retried=job.chunks_retried,
35
35
  peak_mem_mb=job.peak_mem_mb,
36
+ usage=getattr(job, "usage", None) or {}, # ms_flow < 0.8 does not keep it
36
37
  throughput_eps=settled / elapsed if elapsed > 0 else 0.0, # the mean; `_Speeds` gives the recent one
37
38
  started_at=datetime.fromtimestamp(job.started_at),
38
39
  finished_at=datetime.fromtimestamp(job.finished_at) if job.finished_at else None,
@@ -4,7 +4,7 @@ A host is read where it is: this machine with `psutil` (or `/proc`); a node of a
4
4
  from the metrics Ray's agent exports there (`Workspace.node_metrics`), so with no ssh; another one
5
5
  through `ms_env.Host` (the system's `ssh`), with one command that prints `/proc/stat`,
6
6
  `/proc/meminfo` and `nvidia-smi`. The CPU in use is the change of `/proc/stat` between two reads, so the first
7
- read of a host shows none. Each host is read in its own thread, only while the strip is shown,
7
+ read of a host shows none; a Mac, with no `/proc`, says it from `ps`, `sysctl` and `vm_stat`. Each host is read in its own thread, only while the strip is shown,
8
8
  every `interval` seconds: a few for this machine, more for one over the network. The login node
9
9
  of a queue is not read: what it uses says nothing of the nodes its jobs got.
10
10
  """
@@ -30,7 +30,14 @@ INTERVALS = {"local": 5, "ray": 15}
30
30
  _HISTORY = 120 # reads kept per host: 10 min of this machine, 30 of one over the network
31
31
 
32
32
  _GPU_QUERY = "nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits"
33
- _SCRIPT = f"head -1 /proc/stat; grep -E '^(MemTotal|MemAvailable):' /proc/meminfo; {_GPU_QUERY} 2>/dev/null || true"
33
+ # macOS has no /proc: the CPU its processes use now (`ps`, 100 = one core), and memory from
34
+ # `sysctl` and `vm_stat` (free + inactive + speculative pages), said the way Linux says it.
35
+ _MAC = ("n=$(sysctl -n hw.ncpu); ps -A -o %cpu= | awk -v n=$n '{s += $1} END {printf \"cpunow %.1f\\n\", s / n}'; "
36
+ "echo \"MemTotal: $(($(sysctl -n hw.memsize) / 1024)) kB\"; "
37
+ "vm_stat | awk '/page size of/ {p = $8} /^Pages (free|inactive|speculative):/ {a += $NF} "
38
+ "END {printf \"MemAvailable: %d kB\\n\", a * p / 1024}'")
39
+ _SCRIPT = (f"if [ -r /proc/stat ]; then head -1 /proc/stat; grep -E '^(MemTotal|MemAvailable):' /proc/meminfo; "
40
+ f"else {_MAC}; fi; {_GPU_QUERY} 2>/dev/null || true")
34
41
 
35
42
 
36
43
  @dataclass(slots=True)
@@ -72,7 +79,9 @@ def parse(text: str, previous: tuple[int, int] | None) -> tuple[Sample, tuple[in
72
79
  """What `_SCRIPT` printed, and the CPU counters it read (busy, total) for the next read."""
73
80
  sample, ticks, mem = Sample(), None, {}
74
81
  for line in text.splitlines():
75
- if line.startswith("cpu "):
82
+ if line.startswith("cpunow "): # a Mac says its share now, no counters
83
+ sample.cpu = min(100.0, float(line.split()[1]))
84
+ elif line.startswith("cpu "):
76
85
  values = [int(v) for v in line.split()[1:]]
77
86
  idle = values[3] + (values[4] if len(values) > 4 else 0) # idle + iowait
78
87
  ticks = (sum(values) - idle, sum(values))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ms_components
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: PySide6 UI components for MolSuite: smart table, project browser, job monitor, PyMOL dock, steppers.
5
5
  Author: Mario S. Valdés-Tresanco, Mario E. Valdés-Tresanco
6
6
  License-Expression: MIT
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.5.2",
3
+ "distance": 0,
4
+ "node": "g4dde9fadd27ceb43fa1bae25b27e897ed34c7f5c",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-30"
8
+ }
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import subprocess
3
4
  from datetime import datetime
4
5
  from types import SimpleNamespace
5
6
 
@@ -75,6 +76,14 @@ def test_the_detail_shows_the_numbers_the_tasks_and_the_failures_grouped():
75
76
  detail.set_job(_job(status="completed", engine_id=None)) # of another session: no grid, a note
76
77
  assert not detail._grid.isVisibleTo(detail) and "not run by this session" in detail._meta.text()
77
78
  assert detail._fact["status"].text() == "completed"
79
+ assert detail._fact["CPU"].text() == detail._fact["GPU"].text() == "—" # recorded without its usage
80
+
81
+ used = {"cpu_s": 7200.0, "peak_cpu": 4, "busy_s": 5400.0, "gpu_s": 1800.0, "peak_gpu": 1, "vram_mb": 1200,
82
+ "gpu_util": 61.4}
83
+ detail.set_job(_job(status="completed", engine_id=None, started_at=datetime(2026, 1, 1, 10),
84
+ finished_at=datetime(2026, 1, 1, 11), usage=used))
85
+ assert detail._fact["CPU"].text() == "1.50 CPU·h · 1.5 busy of 2.0 held on average · 4 at most"
86
+ assert detail._fact["GPU"].text() == "0.50 GPU·h · 1 at most · 1,200 MB VRAM at most · 61 % busy on average"
78
87
 
79
88
 
80
89
  def test_monitor_widget_lists_filters_and_selects_the_jobs_and_empties_without_a_project():
@@ -110,6 +119,23 @@ def test_monitor_widget_lists_filters_and_selects_the_jobs_and_empties_without_a
110
119
  assert widget._detail._name.text() == "Select a job"
111
120
 
112
121
 
122
+ def test_a_mac_is_read_without_proc(tmp_path):
123
+ """Run the Mac half of the script with stand-ins for its `sysctl`, `ps` and `vm_stat`."""
124
+ from ms_components.ms_monitor.resources import _MAC, parse
125
+
126
+ fakes = {
127
+ "sysctl": 'case "$2" in hw.ncpu) echo 8 ;; hw.memsize) echo 17179869184 ;; esac',
128
+ "ps": "printf ' 100.0\\n 50.0\\n 10.0\\n'", # 160 % of one core = 20 % of eight
129
+ "vm_stat": "printf 'Mach Virtual Memory Statistics: (page size of 16384 bytes)\\nPages free: 65536.\\n"
130
+ "Pages active: 9999.\\nPages inactive: 131072.\\nPages speculative: 65536.\\n'", # 4 GB free
131
+ }
132
+ for name, body in fakes.items():
133
+ (tmp_path / name).write_text(f"#!/bin/sh\n{body}\n")
134
+ (tmp_path / name).chmod(0o755)
135
+ text = subprocess.check_output(["sh", "-c", _MAC], text=True, env={"PATH": f"{tmp_path}:/usr/bin:/bin"})
136
+ sample, _ = parse(text, None)
137
+ assert (sample.cpu, round(sample.mem), round(sample.mem_total_gb)) == (20.0, 75, 16), text
138
+
113
139
  def test_the_hosts_are_listed_once_with_their_executors_and_a_queue_is_not_read():
114
140
  from ms_components.ms_monitor.resources import parse, workspace_hosts
115
141
 
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.5.0",
3
- "distance": 0,
4
- "node": "gd3f2c27fd4e495b9c2d2fb03ebb83b8a2baa0d26",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-29"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes