Skip to content

Commit 3d915f3

Browse files
authored
Resolved client-server import chain and added installation key for instrument server dependencies (#448)
* Moves the `_midpoint()` function to `murfey.util.tomo` to keep client and server modules detached * Replaces remaining instances of `procrunner.run()` with `subprocess.run()` * Creates a new key for instrument server package dependencies
1 parent 19f2f31 commit 3d915f3

File tree

8 files changed

+81
-84
lines changed

8 files changed

+81
-84
lines changed

pyproject.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,16 @@ classifiers = [
3232
dependencies = [
3333
"backports.entry_points_selectable",
3434
"defusedxml", # For safely parsing XML files
35-
"pydantic<2", # Locked to <2 by zocalo
35+
"pydantic<2", # Locked to <2 by cygwin terminal
3636
"requests",
3737
"rich",
3838
"werkzeug",
3939
]
4040
[project.optional-dependencies]
4141
cicd = [
42-
"pytest-cov", # Used by Azure Pipelines for PyTest coverage reports
42+
"pytest-cov", # Used for generating PyTest coverage reports
4343
]
4444
client = [
45-
"procrunner",
4645
"textual==0.42.0",
4746
"websocket-client",
4847
"xmltodict",
@@ -53,8 +52,12 @@ developer = [
5352
"pre-commit", # Formatting, linting, type checking, etc.
5453
"pytest", # Test code functionality
5554
]
55+
instrument-server = [
56+
"fastapi[standard]",
57+
"python-jose[cryptography]",
58+
"uvicorn[standard]",
59+
]
5660
server = [
57-
# "matplotlib", # For visual statistical analysis of images
5861
"aiohttp",
5962
"cryptography",
6063
"fastapi[standard]",

src/murfey/cli/transfer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from __future__ import annotations
22

33
import argparse
4+
import subprocess
45
from pathlib import Path
56
from urllib.parse import urlparse
67

7-
import procrunner
88
import requests
99
from rich.console import Console
1010
from rich.prompt import Confirm
@@ -76,6 +76,6 @@ def run():
7676
cmd.extend(list(Path(args.source or ".").glob("*")))
7777
cmd.append(f"{murfey_url.hostname}::{args.destination}")
7878

79-
result = procrunner.run(cmd)
79+
result = subprocess.run(cmd)
8080
if result.returncode:
8181
console.print(f"[red]rsync failed returning code {result.returncode}")

src/murfey/client/contexts/tomo.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from murfey.util import authorised_requests, capture_post, get_machine_config_client
2323
from murfey.util.mdoc import get_block, get_global_data, get_num_blocks
24+
from murfey.util.tomo import midpoint
2425

2526
logger = logging.getLogger("murfey.client.contexts.tomo")
2627

@@ -64,20 +65,6 @@ def _construct_tilt_series_name(file_path: Path) -> str:
6465
return "_".join(split_name[:-5])
6566

6667

67-
def _midpoint(angles: List[float]) -> int:
68-
if not angles:
69-
return 0
70-
if len(angles) <= 2:
71-
return round(angles[0])
72-
sorted_angles = sorted(angles)
73-
return round(
74-
sorted_angles[len(sorted_angles) // 2]
75-
if sorted_angles[len(sorted_angles) // 2]
76-
and sorted_angles[len(sorted_angles) // 2 + 1]
77-
else 0
78-
)
79-
80-
8168
class ProcessFileIncomplete(BaseModel):
8269
dest: Path
8370
source: Path
@@ -738,7 +725,7 @@ def gather_metadata(
738725
if environment
739726
else None
740727
)
741-
mdoc_metadata["manual_tilt_offset"] = -_midpoint(
728+
mdoc_metadata["manual_tilt_offset"] = -midpoint(
742729
[float(b["TiltAngle"]) for b in blocks]
743730
)
744731
mdoc_metadata["source"] = str(self._basepath)

src/murfey/instrument_server/api.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import secrets
4+
import subprocess
45
import time
56
from datetime import datetime
67
from functools import partial
@@ -9,7 +10,6 @@
910
from typing import Annotated, Dict, List, Optional, Union
1011
from urllib.parse import urlparse
1112

12-
import procrunner
1313
import requests
1414
from fastapi import APIRouter, Depends, HTTPException, status
1515
from fastapi.security import OAuth2PasswordBearer
@@ -21,7 +21,7 @@
2121
from murfey.client.multigrid_control import MultigridController
2222
from murfey.client.rsync import RSyncer
2323
from murfey.client.watchdir_multigrid import MultigridDirWatcher
24-
from murfey.util import sanitise_nonpath, secure_path
24+
from murfey.util import sanitise, sanitise_nonpath, secure_path
2525
from murfey.util.instrument_models import MultigridWatcherSpec
2626
from murfey.util.models import File, Token
2727

@@ -278,19 +278,16 @@ class GainReference(BaseModel):
278278

279279
@router.post("/sessions/{session_id}/upload_gain_reference")
280280
def upload_gain_reference(session_id: MurfeySessionID, gain_reference: GainReference):
281+
safe_gain_path = sanitise(str(gain_reference.gain_path))
282+
safe_visit_path = sanitise(gain_reference.visit_path)
283+
safe_destination_dir = sanitise(gain_reference.gain_destination_dir)
281284
cmd = [
282285
"rsync",
283-
str(gain_reference.gain_path),
284-
f"{urlparse(_get_murfey_url(), allow_fragments=False).hostname}::{gain_reference.visit_path}/{gain_reference.gain_destination_dir}/{secure_filename(gain_reference.gain_path.name)}",
286+
safe_gain_path,
287+
f"{urlparse(_get_murfey_url(), allow_fragments=False).hostname}::{safe_visit_path}/{safe_destination_dir}/{secure_filename(gain_reference.gain_path.name)}",
285288
]
286-
gain_rsync = procrunner.run(cmd)
289+
gain_rsync = subprocess.run(cmd)
287290
if gain_rsync.returncode:
288-
safe_gain_path = (
289-
str(gain_reference.gain_path).replace("\r\n", "").replace("\n", "")
290-
)
291-
safe_visit_path = gain_reference.visit_path.replace("\r\n", "").replace(
292-
"\n", ""
293-
)
294291
logger.warning(
295292
f"Gain reference file {safe_gain_path} was not successfully transferred to {safe_visit_path}/processing"
296293
)

src/murfey/server/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import murfey.server.prometheus as prom
4949
import murfey.server.websocket
5050
import murfey.util.db as db
51-
from murfey.client.contexts.tomo import _midpoint
5251
from murfey.server.murfey_db import url # murfey_db
5352
from murfey.util import LogFilter
5453
from murfey.util.config import (
@@ -60,6 +59,7 @@
6059
)
6160
from murfey.util.processing_params import default_spa_parameters
6261
from murfey.util.state import global_state
62+
from murfey.util.tomo import midpoint
6363

6464
try:
6565
from murfey.server.ispyb import TransportManager # Session
@@ -2576,7 +2576,7 @@ def feedback_callback(header: dict, message: dict) -> None:
25762576
)
25772577
if not stack_file.parent.exists():
25782578
stack_file.parent.mkdir(parents=True)
2579-
tilt_offset = _midpoint([float(get_angle(t)) for t in tilts])
2579+
tilt_offset = midpoint([float(get_angle(t)) for t in tilts])
25802580
zocalo_message = {
25812581
"recipes": ["em-tomo-align"],
25822582
"parameters": {

src/murfey/server/api/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import murfey.server.websocket as ws
3434
import murfey.util.eer
3535
from murfey.server import (
36-
_midpoint,
3736
_murfey_id,
3837
_transport_object,
3938
check_tilt_series_mc,
@@ -108,6 +107,7 @@
108107
)
109108
from murfey.util.processing_params import default_spa_parameters
110109
from murfey.util.state import global_state
110+
from murfey.util.tomo import midpoint
111111

112112
log = logging.getLogger("murfey.server.api")
113113

@@ -840,7 +840,7 @@ def register_completed_tilt_series(
840840
)
841841
if not stack_file.parent.exists():
842842
stack_file.parent.mkdir(parents=True)
843-
tilt_offset = _midpoint([float(get_angle(t)) for t in tilts])
843+
tilt_offset = midpoint([float(get_angle(t)) for t in tilts])
844844
zocalo_message = {
845845
"recipes": ["em-tomo-align"],
846846
"parameters": {

src/murfey/util/rsync.py

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from __future__ import annotations
22

33
import logging
4+
import subprocess
45
from pathlib import Path
56
from typing import Callable, Dict, List, Optional, Tuple, Union
67

7-
import procrunner
8-
98
from murfey.util import Processor
109
from murfey.util.file_monitor import Monitor
1110

@@ -32,7 +31,7 @@ def __init__(
3231
self.received_bytes = 0
3332
self.byte_rate: float = 0
3433
self.total_size = 0
35-
self.runner_return: List[procrunner.ReturnObject] = []
34+
self.runner_return: List[subprocess.CompletedProcess] = []
3635
self._root = root
3736
self._sub_structure: Optional[Path] = None
3837
self._notify = notify or (lambda f: None)
@@ -53,7 +52,7 @@ def _run_rsync(
5352
retry: bool = True,
5453
):
5554
"""
56-
Run rsync -v on a list of files using procrunner.
55+
Run rsync -v on a list of files using subprocess.
5756
5857
:param root: root path of files for transferring; structure below the root is preserved
5958
:type root: pathlib.Path object
@@ -109,69 +108,64 @@ def _single_rsync(
109108
else:
110109
cmd.append(str(self._finaldir / sub_struct) + "/")
111110
self._transferring = True
112-
runner = procrunner.run(
111+
runner = subprocess.run(
113112
cmd,
114-
callback_stdout=self._parse_rsync_stdout,
115-
callback_stderr=self._parse_rsync_stderr,
113+
capture_output=True,
116114
)
115+
for line in runner.stdout.decode("utf-8", "replace").split("\n"):
116+
self._parse_rsync_stdout(line)
117+
for line in runner.stderr.decode("utf-8", "replace").split("\n"):
118+
self._parse_rsync_stderr(line)
117119
self.runner_return.append(runner)
118120
self.failed.extend(root / sub_struct / f for f in self._failed_tmp)
119121
if retry:
120122
self._in.put(root / sub_struct / f for f in self._failed_tmp)
121123

122-
def _parse_rsync_stdout(self, stdout: bytes):
124+
def _parse_rsync_stdout(self, line: str):
123125
"""
124126
Parse rsync stdout to collect information such as the paths of transferred
125127
files and the amount of data transferred.
126128
127129
:param stdout: stdout of rsync process
128130
:type stdout: bytes
129131
"""
130-
stringy_stdout = str(stdout)
131-
if stringy_stdout:
132-
if self._transferring:
133-
if stringy_stdout.startswith("sent"):
134-
self._transferring = False
135-
byte_info = stringy_stdout.split()
136-
self.sent_bytes = int(
137-
byte_info[byte_info.index("sent") + 1].replace(",", "")
138-
)
139-
self.received_bytes = int(
140-
byte_info[byte_info.index("received") + 1].replace(",", "")
141-
)
142-
self.byte_rate = float(
143-
byte_info[byte_info.index("bytes/sec") - 1].replace(",", "")
144-
)
145-
elif len(stringy_stdout.split()) == 1:
146-
if self._root and self._sub_structure:
147-
self._notify(
148-
self._finaldir / self._sub_structure / stringy_stdout
149-
)
150-
self._out.put(self._root / self._sub_structure / stringy_stdout)
151-
else:
152-
logger.warning(
153-
f"root or substructure not set for transfer of {stringy_stdout}"
154-
)
155-
else:
156-
if "total size" in stringy_stdout:
157-
self.total_size = int(
158-
stringy_stdout.replace("total size", "").split()[1]
132+
if self._transferring:
133+
if line.startswith("sent"):
134+
self._transferring = False
135+
byte_info = line.split()
136+
self.sent_bytes = int(
137+
byte_info[byte_info.index("sent") + 1].replace(",", "")
138+
)
139+
self.received_bytes = int(
140+
byte_info[byte_info.index("received") + 1].replace(",", "")
141+
)
142+
self.byte_rate = float(
143+
byte_info[byte_info.index("bytes/sec") - 1].replace(",", "")
144+
)
145+
elif len(line.split()) == 1:
146+
if self._root and self._sub_structure:
147+
self._notify(self._finaldir / self._sub_structure / line)
148+
self._out.put(self._root / self._sub_structure / line)
149+
else:
150+
logger.warning(
151+
f"root or substructure not set for transfer of {line}"
159152
)
153+
else:
154+
if "total size" in line:
155+
self.total_size = int(line.replace("total size", "").split()[1])
160156

161-
def _parse_rsync_stderr(self, stderr: bytes):
157+
def _parse_rsync_stderr(self, line: str):
162158
"""
163159
Parse rsync stderr to collect information on any files that failed to transfer.
164160
165161
:param stderr: stderr of rsync process
166162
:type stderr: bytes
167163
"""
168-
stringy_stderr = str(stderr)
169-
if stringy_stderr:
170-
if (
171-
stringy_stderr.startswith("rsync: link_stat")
172-
or stringy_stderr.startswith("rsync: [sender] link_stat")
173-
) and "failed" in stringy_stderr:
174-
failed_msg = stringy_stderr.split()
175-
self._failed_tmp.append(
176-
failed_msg[failed_msg.index("failed:") - 1].replace('"', "")
177-
)
164+
if (
165+
line.startswith("rsync: link_stat")
166+
or line.startswith("rsync: [sender] link_stat")
167+
) and "failed" in line:
168+
failed_msg = line.split()
169+
self._failed_tmp.append(
170+
failed_msg[failed_msg.index("failed:") - 1].replace('"', "")
171+
)

src/murfey/util/tomo.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
def midpoint(angles: list[float]) -> int:
2+
"""
3+
Utility function to calculate the midpoint of the angles used in a tilt series.
4+
Used primarily in the tomography workflow.
5+
"""
6+
if not angles:
7+
return 0
8+
if len(angles) <= 2:
9+
return round(angles[0])
10+
sorted_angles = sorted(angles)
11+
return round(
12+
sorted_angles[len(sorted_angles) // 2]
13+
if sorted_angles[len(sorted_angles) // 2]
14+
and sorted_angles[len(sorted_angles) // 2 + 1]
15+
else 0
16+
)

0 commit comments

Comments
 (0)