Skip to content

Commit 4a235bd

Browse files
committed
Migrated client-side only functions out into a separate utils module
1 parent 8567d8d commit 4a235bd

File tree

2 files changed

+161
-149
lines changed

2 files changed

+161
-149
lines changed

src/murfey/util/__init__.py

Lines changed: 1 addition & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,19 @@
11
from __future__ import annotations
22

33
import asyncio
4-
import configparser
5-
import copy
64
import inspect
7-
import json
85
import logging
9-
import os
10-
import shutil
11-
from functools import lru_cache, partial
126
from pathlib import Path
137
from queue import Queue
148
from threading import Thread
15-
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union
16-
from urllib.parse import ParseResult, urlparse, urlunparse
9+
from typing import Awaitable, Callable, Optional
1710
from uuid import uuid4
1811

19-
import requests
2012
from werkzeug.utils import secure_filename
2113

22-
from murfey.util.models import Visit
23-
2414
logger = logging.getLogger("murfey.util")
2515

2616

27-
def read_config() -> configparser.ConfigParser:
28-
config = configparser.ConfigParser()
29-
try:
30-
mcch = os.environ.get("MURFEY_CLIENT_CONFIG_HOME")
31-
murfey_client_config_home = Path(mcch) if mcch else Path.home()
32-
with open(murfey_client_config_home / ".murfey") as configfile:
33-
config.read_file(configfile)
34-
except FileNotFoundError:
35-
logger.warning(
36-
f"Murfey client configuration file {murfey_client_config_home / '.murfey'} not found"
37-
)
38-
if "Murfey" not in config:
39-
config["Murfey"] = {}
40-
return config
41-
42-
43-
@lru_cache(maxsize=1)
44-
def get_machine_config_client(
45-
url: str, instrument_name: str = "", demo: bool = False
46-
) -> dict:
47-
_instrument_name: str | None = instrument_name or os.getenv("BEAMLINE")
48-
if not _instrument_name:
49-
return {}
50-
return requests.get(f"{url}/instruments/{_instrument_name}/machine").json()
51-
52-
53-
def authorised_requests() -> Tuple[Callable, Callable, Callable, Callable]:
54-
token = read_config()["Murfey"].get("token", "")
55-
_get = partial(requests.get, headers={"Authorization": f"Bearer {token}"})
56-
_post = partial(requests.post, headers={"Authorization": f"Bearer {token}"})
57-
_put = partial(requests.put, headers={"Authorization": f"Bearer {token}"})
58-
_delete = partial(requests.delete, headers={"Authorization": f"Bearer {token}"})
59-
return _get, _post, _put, _delete
60-
61-
62-
requests.get, requests.post, requests.put, requests.delete = authorised_requests()
63-
64-
6517
def sanitise(in_string: str) -> str:
6618
return in_string.replace("\r\n", "").replace("\n", "")
6719

@@ -113,106 +65,6 @@ def posix_path(path: Path) -> str:
11365
return str(path)
11466

11567

116-
def _get_visit_list(api_base: ParseResult, instrument_name: str):
117-
get_visits_url = api_base._replace(
118-
path=f"/instruments/{instrument_name}/visits_raw"
119-
)
120-
server_reply = requests.get(get_visits_url.geturl())
121-
if server_reply.status_code != 200:
122-
raise ValueError(f"Server unreachable ({server_reply.status_code})")
123-
return [Visit.parse_obj(v) for v in server_reply.json()]
124-
125-
126-
def capture_post(url: str, json: dict | list = {}) -> requests.Response | None:
127-
try:
128-
response = requests.post(url, json=json)
129-
except Exception as e:
130-
logger.error(f"Exception encountered in post to {url}: {e}")
131-
response = requests.Response()
132-
if response.status_code != 200:
133-
logger.warning(
134-
f"Response to post to {url} with data {json} had status code "
135-
f"{response.status_code}. The reason given was {response.reason}"
136-
)
137-
split_url = urlparse(url)
138-
client_config = read_config()
139-
failure_url = urlunparse(
140-
split_url._replace(
141-
path=f"/instruments/{client_config['Murfey']['instrument_name']}/failed_client_post"
142-
)
143-
)
144-
try:
145-
resend_response = requests.post(
146-
failure_url, json={"url": url, "data": json}
147-
)
148-
except Exception as e:
149-
logger.error(f"Exception encountered in post to {failure_url}: {e}")
150-
resend_response = requests.Response()
151-
if resend_response.status_code != 200:
152-
logger.warning(
153-
f"Response to post to {failure_url} failed with {resend_response.reason}"
154-
)
155-
156-
return response
157-
158-
159-
def capture_get(url: str) -> requests.Response | None:
160-
try:
161-
response = requests.get(url)
162-
except Exception as e:
163-
logger.error(f"Exception encountered in get from {url}: {e}")
164-
response = None
165-
if response and response.status_code != 200:
166-
logger.warning(
167-
f"Response to get from {url} had status code {response.status_code}. "
168-
f"The reason given was {response.reason}"
169-
)
170-
return response
171-
172-
173-
def set_default_acquisition_output(
174-
new_output_dir: Path,
175-
software_settings_output_directories: Dict[str, List[str]],
176-
safe: bool = True,
177-
):
178-
for p, keys in software_settings_output_directories.items():
179-
if safe:
180-
settings_copy_path = Path(p)
181-
settings_copy_path = settings_copy_path.parent / (
182-
"_murfey_" + settings_copy_path.name
183-
)
184-
shutil.copy(p, str(settings_copy_path))
185-
with open(p, "r") as for_parsing:
186-
settings = json.load(for_parsing)
187-
# for safety
188-
settings_copy = copy.deepcopy(settings)
189-
190-
def _set(d: dict, keys_list: List[str], value: str) -> dict:
191-
if len(keys_list) > 1:
192-
tmp_value: Union[dict, str] = _set(
193-
d[keys_list[0]], keys_list[1:], value
194-
)
195-
else:
196-
tmp_value = value
197-
return {_k: tmp_value if _k == keys_list[0] else _v for _k, _v in d.items()}
198-
199-
settings_copy = _set(settings_copy, keys, str(new_output_dir))
200-
201-
def _check_dict_structure(d1: dict, d2: dict) -> bool:
202-
if set(d1.keys()) != set(d2.keys()):
203-
return False
204-
for k in d1.keys():
205-
if isinstance(d1[k], dict):
206-
if not isinstance(d2[k], dict):
207-
return False
208-
_check_dict_structure(d1[k], d2[k])
209-
return True
210-
211-
if _check_dict_structure(settings, settings_copy):
212-
with open(p, "w") as sf:
213-
json.dump(settings_copy, sf)
214-
215-
21668
class Observer:
21769
"""
21870
A helper class implementing the observer pattern supporting both

src/murfey/util/client.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
Utility functions used solely by the Murfey client. They help set up its
3+
configuration, communicate with the backend server using the correct credentials,
4+
and set default directories to work with.
5+
"""
6+
7+
import configparser
8+
import copy
9+
import json
10+
import logging
11+
import os
12+
import shutil
13+
from functools import lru_cache, partial
14+
from pathlib import Path
15+
from typing import Callable, Optional, Union
16+
from urllib.parse import ParseResult, urlparse, urlunparse
17+
18+
import requests
19+
20+
from murfey.util.models import Visit
21+
22+
logger = logging.getLogger("murfey.util.client")
23+
24+
25+
def read_config() -> configparser.ConfigParser:
26+
config = configparser.ConfigParser()
27+
try:
28+
mcch = os.environ.get("MURFEY_CLIENT_CONFIG_HOME")
29+
murfey_client_config_home = Path(mcch) if mcch else Path.home()
30+
with open(murfey_client_config_home / ".murfey") as configfile:
31+
config.read_file(configfile)
32+
except FileNotFoundError:
33+
logger.warning(
34+
f"Murfey client configuration file {murfey_client_config_home / '.murfey'} not found"
35+
)
36+
if "Murfey" not in config:
37+
config["Murfey"] = {}
38+
return config
39+
40+
41+
@lru_cache(maxsize=1)
42+
def get_machine_config_client(
43+
url: str, instrument_name: str = "", demo: bool = False
44+
) -> dict:
45+
_instrument_name: Optional[str] = instrument_name or os.getenv("BEAMLINE")
46+
if not _instrument_name:
47+
return {}
48+
return requests.get(f"{url}/instruments/{_instrument_name}/machine").json()
49+
50+
51+
def authorised_requests() -> tuple[Callable, Callable, Callable, Callable]:
52+
token = read_config()["Murfey"].get("token", "")
53+
_get = partial(requests.get, headers={"Authorization": f"Bearer {token}"})
54+
_post = partial(requests.post, headers={"Authorization": f"Bearer {token}"})
55+
_put = partial(requests.put, headers={"Authorization": f"Bearer {token}"})
56+
_delete = partial(requests.delete, headers={"Authorization": f"Bearer {token}"})
57+
return _get, _post, _put, _delete
58+
59+
60+
requests.get, requests.post, requests.put, requests.delete = authorised_requests()
61+
62+
63+
def _get_visit_list(api_base: ParseResult, instrument_name: str):
64+
get_visits_url = api_base._replace(
65+
path=f"/instruments/{instrument_name}/visits_raw"
66+
)
67+
server_reply = requests.get(get_visits_url.geturl())
68+
if server_reply.status_code != 200:
69+
raise ValueError(f"Server unreachable ({server_reply.status_code})")
70+
return [Visit.parse_obj(v) for v in server_reply.json()]
71+
72+
73+
def capture_post(url: str, json: Union[dict, list] = {}) -> Optional[requests.Response]:
74+
try:
75+
response = requests.post(url, json=json)
76+
except Exception as e:
77+
logger.error(f"Exception encountered in post to {url}: {e}")
78+
response = requests.Response()
79+
if response.status_code != 200:
80+
logger.warning(
81+
f"Response to post to {url} with data {json} had status code "
82+
f"{response.status_code}. The reason given was {response.reason}"
83+
)
84+
split_url = urlparse(url)
85+
client_config = read_config()
86+
failure_url = urlunparse(
87+
split_url._replace(
88+
path=f"/instruments/{client_config['Murfey']['instrument_name']}/failed_client_post"
89+
)
90+
)
91+
try:
92+
resend_response = requests.post(
93+
failure_url, json={"url": url, "data": json}
94+
)
95+
except Exception as e:
96+
logger.error(f"Exception encountered in post to {failure_url}: {e}")
97+
resend_response = requests.Response()
98+
if resend_response.status_code != 200:
99+
logger.warning(
100+
f"Response to post to {failure_url} failed with {resend_response.reason}"
101+
)
102+
103+
return response
104+
105+
106+
def capture_get(url: str) -> Optional[requests.Response]:
107+
try:
108+
response = requests.get(url)
109+
except Exception as e:
110+
logger.error(f"Exception encountered in get from {url}: {e}")
111+
response = None
112+
if response and response.status_code != 200:
113+
logger.warning(
114+
f"Response to get from {url} had status code {response.status_code}. "
115+
f"The reason given was {response.reason}"
116+
)
117+
return response
118+
119+
120+
def set_default_acquisition_output(
121+
new_output_dir: Path,
122+
software_settings_output_directories: dict[str, list[str]],
123+
safe: bool = True,
124+
):
125+
for p, keys in software_settings_output_directories.items():
126+
if safe:
127+
settings_copy_path = Path(p)
128+
settings_copy_path = settings_copy_path.parent / (
129+
"_murfey_" + settings_copy_path.name
130+
)
131+
shutil.copy(p, str(settings_copy_path))
132+
with open(p, "r") as for_parsing:
133+
settings = json.load(for_parsing)
134+
# for safety
135+
settings_copy = copy.deepcopy(settings)
136+
137+
def _set(d: dict, keys_list: list[str], value: str) -> dict:
138+
if len(keys_list) > 1:
139+
tmp_value: Union[dict, str] = _set(
140+
d[keys_list[0]], keys_list[1:], value
141+
)
142+
else:
143+
tmp_value = value
144+
return {_k: tmp_value if _k == keys_list[0] else _v for _k, _v in d.items()}
145+
146+
settings_copy = _set(settings_copy, keys, str(new_output_dir))
147+
148+
def _check_dict_structure(d1: dict, d2: dict) -> bool:
149+
if set(d1.keys()) != set(d2.keys()):
150+
return False
151+
for k in d1.keys():
152+
if isinstance(d1[k], dict):
153+
if not isinstance(d2[k], dict):
154+
return False
155+
_check_dict_structure(d1[k], d2[k])
156+
return True
157+
158+
if _check_dict_structure(settings, settings_copy):
159+
with open(p, "w") as sf:
160+
json.dump(settings_copy, sf)

0 commit comments

Comments
 (0)