Skip to content

Commit a9dcf9a

Browse files
authored
Fixed Murfey's Auto-Update Function and Improved Pytest Functionality (#625)
* Migrated contents of 'murfey.client.__init__' to new module 'murfey.client.tui.main', since its contents are related to the initialisation of the TUI app * Refactored 'murfey.instrument_server.__init__' so that the Murfey update checker is run through to completion before the components needed to update the server are imported and initialised, thereby avoiding the files associated with the process from becoming locked * Lock FastAPI to <0.116.0, as 'fastapi-cloud-cli', which was included as a dependency under the 'standard' FastAPI package in 0.116.0, currently uses 'httpx'<0.28.0, which causes a clash with 'bump-my-version', which uses 'httpx'>=0.28.0; updates the 'murfey.client' entry point to point to where the function to initiliase the TUI app has been migrated to * Skip ISPyB and Murfey database-related tests if they haven't been set up properly; this allows us to run Pytest locally again even without test databases configured
1 parent 59d2aa4 commit a9dcf9a

File tree

14 files changed

+703
-498
lines changed

14 files changed

+703
-498
lines changed

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,17 @@ developer = [
5252
"ipykernel", # Enable interactive coding with VS Code and Jupyter Notebook
5353
"pre-commit", # Formatting, linting, type checking, etc.
5454
"pytest", # Test code functionality
55+
"pytest-mock", # Additional mocking tools for unit tests
5556
]
5657
instrument-server = [
5758
"aiohttp",
58-
"fastapi[standard]",
59+
"fastapi[standard]<0.116.0",
5960
"python-jose",
6061
]
6162
server = [
6263
"aiohttp",
6364
"cryptography",
64-
"fastapi[standard]",
65+
"fastapi[standard]<0.116.0",
6566
"ispyb>=10.2.4", # Responsible for setting requirements for SQLAlchemy and mysql-connector-python;
6667
"jinja2",
6768
"mrcfile",
@@ -82,7 +83,7 @@ Documentation = "https://github.com/DiamondLightSource/python-murfey"
8283
GitHub = "https://github.com/DiamondLightSource/python-murfey"
8384
[project.scripts]
8485
"murfey.add_user" = "murfey.cli.add_user:run"
85-
"murfey.client" = "murfey.client:run"
86+
"murfey.client" = "murfey.client.tui.main:run"
8687
"murfey.create_db" = "murfey.cli.create_db:run"
8788
"murfey.db_sql" = "murfey.cli.murfey_db_sql:run"
8889
"murfey.decrypt_password" = "murfey.cli.decrypt_db_password:run"

src/murfey/__main__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/murfey/client/__init__.py

Lines changed: 0 additions & 348 deletions
Original file line numberDiff line numberDiff line change
@@ -1,348 +0,0 @@
1-
from __future__ import annotations
2-
3-
import argparse
4-
import configparser
5-
import logging
6-
import os
7-
import platform
8-
import shutil
9-
import sys
10-
import time
11-
import webbrowser
12-
from datetime import datetime
13-
from pathlib import Path
14-
from pprint import pprint
15-
from queue import Queue
16-
from typing import Literal
17-
from urllib.parse import ParseResult, urlparse
18-
19-
import requests
20-
from rich.prompt import Confirm
21-
22-
import murfey.client.update
23-
import murfey.client.watchdir
24-
import murfey.client.websocket
25-
from murfey.client.customlogging import CustomHandler, DirectableRichHandler
26-
from murfey.client.instance_environment import MurfeyInstanceEnvironment
27-
from murfey.client.tui.app import MurfeyTUI
28-
from murfey.client.tui.status_bar import StatusBar
29-
from murfey.util.api import url_path_for
30-
from murfey.util.client import authorised_requests, read_config
31-
from murfey.util.models import Visit
32-
33-
log = logging.getLogger("murfey.client")
34-
35-
requests.get, requests.post, requests.put, requests.delete = authorised_requests()
36-
37-
38-
def _get_visit_list(api_base: ParseResult, instrument_name: str):
39-
proxy_path = api_base.path.rstrip("/")
40-
get_visits_url = api_base._replace(
41-
path=f"{proxy_path}{url_path_for('session_control.router', 'get_current_visits', instrument_name=instrument_name)}"
42-
)
43-
server_reply = requests.get(get_visits_url.geturl())
44-
if server_reply.status_code != 200:
45-
raise ValueError(f"Server unreachable ({server_reply.status_code})")
46-
return [Visit.parse_obj(v) for v in server_reply.json()]
47-
48-
49-
def write_config(config: configparser.ConfigParser):
50-
mcch = os.environ.get("MURFEY_CLIENT_CONFIG_HOME")
51-
murfey_client_config_home = Path(mcch) if mcch else Path.home()
52-
with open(murfey_client_config_home / ".murfey", "w") as configfile:
53-
config.write(configfile)
54-
55-
56-
def main_loop(
57-
source_watchers: list[murfey.client.watchdir.DirWatcher],
58-
appearance_time: float,
59-
transfer_all: bool,
60-
):
61-
log.info(
62-
f"Murfey {murfey.__version__} on Python {'.'.join(map(str, sys.version_info[0:3]))} entering main loop"
63-
)
64-
if appearance_time > 0:
65-
modification_time: float | None = time.time() - appearance_time * 3600
66-
else:
67-
modification_time = None
68-
while True:
69-
for sw in source_watchers:
70-
sw.scan(modification_time=modification_time, transfer_all=transfer_all)
71-
time.sleep(15)
72-
73-
74-
def _enable_webbrowser_in_cygwin():
75-
"""Helper function to make webbrowser.open() work in CygWin"""
76-
if "cygwin" in platform.system().lower() and shutil.which("cygstart"):
77-
webbrowser.register("cygstart", None, webbrowser.GenericBrowser("cygstart"))
78-
79-
80-
def _check_for_updates(
81-
server: ParseResult, install_version: None | Literal[True] | str
82-
):
83-
if install_version is True:
84-
# User requested installation of the newest version
85-
try:
86-
murfey.client.update.check(server, force=True)
87-
print("\nYou are already running the newest version of Murfey")
88-
exit()
89-
except Exception as e:
90-
exit(f"Murfey update check failed with {e}")
91-
92-
if install_version:
93-
# User requested installation of a specific version
94-
if murfey.client.update.install_murfey(server, install_version):
95-
print(f"\nMurfey has been updated to version {install_version}")
96-
exit()
97-
else:
98-
exit("Error occurred while updating Murfey")
99-
100-
# Otherwise run a routine update check to ensure client and server are compatible
101-
try:
102-
murfey.client.update.check(server)
103-
except Exception as e:
104-
print(f"Murfey update check failed with {e}")
105-
106-
107-
def run():
108-
# Load client config and server information
109-
config = read_config()
110-
instrument_name = config["Murfey"]["instrument_name"]
111-
try:
112-
server_routing = config["ServerRouter"]
113-
except KeyError:
114-
server_routing = {}
115-
server_routing_prefix_found = False
116-
if server_routing:
117-
for path_prefix, server in server_routing.items():
118-
if str(Path.cwd()).startswith(path_prefix):
119-
known_server = server
120-
server_routing_prefix_found = True
121-
break
122-
else:
123-
known_server = None
124-
else:
125-
known_server = config["Murfey"].get("server")
126-
127-
# Set up argument parser with dynamic defaults based on client config
128-
parser = argparse.ArgumentParser(description="Start the Murfey client")
129-
parser.add_argument(
130-
"--server",
131-
metavar="HOST:PORT",
132-
type=str,
133-
help=f"Murfey server to connect to ({known_server})",
134-
default=known_server,
135-
)
136-
parser.add_argument("--visit", help="Name of visit")
137-
parser.add_argument(
138-
"--source", help="Directory to transfer files from", type=Path, default="."
139-
)
140-
parser.add_argument(
141-
"--destination",
142-
help="Directory to transfer files to (syntax: 'data/2022/cm31093-2/tmp/murfey')",
143-
)
144-
parser.add_argument(
145-
"--update",
146-
metavar="VERSION",
147-
nargs="?",
148-
default=None,
149-
const=True,
150-
help="Update Murfey to the newest or to a specific version",
151-
)
152-
parser.add_argument(
153-
"--demo",
154-
action="store_true",
155-
)
156-
parser.add_argument(
157-
"--appearance-time",
158-
type=float,
159-
default=-1,
160-
help="Only consider top level directories that have appeared more recently than this many hours ago",
161-
)
162-
parser.add_argument(
163-
"--fake-dc",
164-
action="store_true",
165-
default=False,
166-
help="Do not perform data collection related calls to API (avoids database inserts)",
167-
)
168-
parser.add_argument(
169-
"--time-based-transfer",
170-
action="store_true",
171-
help="Transfer new files",
172-
)
173-
parser.add_argument(
174-
"--no-transfer",
175-
action="store_true",
176-
help="Avoid actually transferring files",
177-
)
178-
parser.add_argument(
179-
"--debug",
180-
action="store_true",
181-
help="Turn on debugging logs",
182-
)
183-
parser.add_argument(
184-
"--local",
185-
action="store_true",
186-
default=False,
187-
help="Perform rsync transfers locally rather than remotely",
188-
)
189-
parser.add_argument(
190-
"--ignore-mdoc-metadata",
191-
action="store_true",
192-
default=False,
193-
help="Do not attempt to read metadata from all mdoc files",
194-
)
195-
parser.add_argument(
196-
"--remove-files",
197-
action="store_true",
198-
default=False,
199-
help="Remove source files immediately after their transfer",
200-
)
201-
parser.add_argument(
202-
"--name",
203-
type=str,
204-
default="",
205-
help="Name of Murfey session to be created",
206-
)
207-
parser.add_argument(
208-
"--skip-existing-processing",
209-
action="store_true",
210-
default=False,
211-
help="Do not trigger processing for any data directories currently on disk (you may have started processing for them in a previous murfey run)",
212-
)
213-
args = parser.parse_args()
214-
215-
# Logic to exit early based on parsed args
216-
if not args.server:
217-
exit("Murfey server not set. Please run with --server host:port")
218-
if not args.server.startswith(("http://", "https://")):
219-
if "://" in args.server:
220-
exit("Unknown server protocol. Only http:// and https:// are allowed")
221-
args.server = f"http://{args.server}"
222-
if args.remove_files:
223-
remove_prompt = Confirm.ask(
224-
f"Are you sure you want to remove files from {args.source or Path('.').absolute()}?"
225-
)
226-
if not remove_prompt:
227-
exit("Exiting")
228-
229-
# If a new server URL is provided, save info to config file
230-
murfey_url = urlparse(args.server, allow_fragments=False)
231-
if args.server != known_server:
232-
# New server specified. Verify that it is real
233-
print(f"Attempting to connect to new server {args.server}")
234-
try:
235-
murfey.client.update.check(murfey_url, install=False)
236-
except Exception as e:
237-
exit(f"Could not reach Murfey server at {args.server!r} - {e}")
238-
239-
# If server is reachable then update the configuration
240-
config["Murfey"]["server"] = args.server
241-
write_config(config)
242-
243-
# If user requested installation of a specific or a newer version then
244-
# make that happen, otherwise ensure client and server are compatible and
245-
# update if necessary.
246-
_check_for_updates(server=murfey_url, install_version=args.update)
247-
248-
if args.no_transfer:
249-
log.info("No files will be transferred as --no-transfer flag was specified")
250-
251-
# Check ISPyB (if set up) for ongoing visits
252-
ongoing_visits = []
253-
if args.visit:
254-
ongoing_visits = [args.visit]
255-
elif server_routing_prefix_found:
256-
for part in Path.cwd().parts:
257-
if "-" in part:
258-
ongoing_visits = [part]
259-
break
260-
if not ongoing_visits:
261-
print("Ongoing visits:")
262-
ongoing_visits = _get_visit_list(murfey_url, instrument_name)
263-
pprint(ongoing_visits)
264-
ongoing_visits = [v.name for v in ongoing_visits]
265-
266-
_enable_webbrowser_in_cygwin()
267-
268-
# Set up additional log handlers
269-
log.setLevel(logging.DEBUG)
270-
log_queue = Queue()
271-
input_queue = Queue()
272-
273-
# Rich-based console handler
274-
rich_handler = DirectableRichHandler(enable_link_path=False)
275-
rich_handler.setLevel(logging.DEBUG if args.debug else logging.INFO)
276-
277-
# Set up websocket app and handler
278-
client_id_response = requests.get(
279-
f"{murfey_url.geturl()}{url_path_for('session_control.router', 'new_client_id')}"
280-
)
281-
if client_id_response.status_code == 401:
282-
exit(
283-
"This instrument is not authorised to run the TUI app; please use the "
284-
"Murfey web UI instead"
285-
)
286-
elif client_id_response.status_code != 200:
287-
exit(
288-
"Unable to establish connection to Murfey server: \n"
289-
f"{client_id_response.json()}"
290-
)
291-
client_id: dict = client_id_response.json()
292-
ws = murfey.client.websocket.WSApp(
293-
server=args.server,
294-
id=client_id["new_id"],
295-
)
296-
ws_handler = CustomHandler(ws.send)
297-
298-
# Add additional handlers and set logging levels
299-
logging.getLogger().addHandler(rich_handler)
300-
logging.getLogger().addHandler(ws_handler)
301-
logging.getLogger("murfey").setLevel(logging.INFO)
302-
logging.getLogger("websocket").setLevel(logging.WARNING)
303-
304-
log.info("Starting Websocket connection")
305-
306-
# Load machine data for subsequent sections
307-
machine_data = requests.get(
308-
f"{murfey_url.geturl()}{url_path_for('session_control.router', 'machine_info_by_instrument', instrument_name=instrument_name)}"
309-
).json()
310-
gain_ref: Path | None = None
311-
312-
# Set up Murfey environment instance and map it to websocket app
313-
instance_environment = MurfeyInstanceEnvironment(
314-
url=murfey_url,
315-
client_id=ws.id,
316-
instrument_name=instrument_name,
317-
software_versions=machine_data.get("software_versions", {}),
318-
# sources=[Path(args.source)],
319-
# watchers=source_watchers,
320-
default_destination=args.destination or str(datetime.now().year),
321-
demo=args.demo,
322-
processing_only_mode=server_routing_prefix_found,
323-
rsync_url=(
324-
urlparse(machine_data["rsync_url"]).hostname
325-
if machine_data.get("rsync_url")
326-
else ""
327-
),
328-
)
329-
ws.environment = instance_environment
330-
331-
# Set up and run Murfey TUI app
332-
status_bar = StatusBar()
333-
rich_handler.redirect = True
334-
app = MurfeyTUI(
335-
environment=instance_environment,
336-
visits=ongoing_visits,
337-
queues={"input": input_queue, "logs": log_queue},
338-
status_bar=status_bar,
339-
dummy_dc=args.fake_dc,
340-
do_transfer=not args.no_transfer,
341-
gain_ref=gain_ref,
342-
redirected_logger=rich_handler,
343-
force_mdoc_metadata=not args.ignore_mdoc_metadata,
344-
processing_enabled=machine_data.get("processing_enabled", True),
345-
skip_existing_processing=args.skip_existing_processing,
346-
)
347-
app.run()
348-
rich_handler.redirect = False

src/murfey/client/multigrid_control.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ def __post_init__(self):
7171
client_id=0,
7272
murfey_session=self.session_id,
7373
software_versions=machine_data.get("software_versions", {}),
74-
default_destination=f"{datetime.now().year}",
7574
demo=self.demo,
7675
visit=self.visit,
7776
dose_per_frame=self.data_collection_parameters.get("dose_per_frame"),

src/murfey/client/tui/__main__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)