webmo 1.4.2__tar.gz → 1.6.0__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.
- webmo-1.6.0/PKG-INFO +35 -0
- {webmo-1.4.2 → webmo-1.6.0}/setup.py +1 -1
- {webmo-1.4.2 → webmo-1.6.0}/webmo/__init__.py +1 -1
- {webmo-1.4.2 → webmo-1.6.0}/webmo/util.py +83 -12
- {webmo-1.4.2 → webmo-1.6.0}/webmo/webmo_rest.py +73 -31
- webmo-1.6.0/webmo.egg-info/PKG-INFO +35 -0
- webmo-1.4.2/PKG-INFO +0 -15
- webmo-1.4.2/webmo.egg-info/PKG-INFO +0 -15
- {webmo-1.4.2 → webmo-1.6.0}/LICENSE +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/README.md +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/setup.cfg +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/webmo/gui.py +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/webmo/spectrum.py +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/webmo.egg-info/SOURCES.txt +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/webmo.egg-info/dependency_links.txt +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/webmo.egg-info/requires.txt +0 -0
- {webmo-1.4.2 → webmo-1.6.0}/webmo.egg-info/top_level.txt +0 -0
webmo-1.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webmo
|
|
3
|
+
Version: 1.6.0
|
|
4
|
+
Summary: A Python-based interface to WebMO
|
|
5
|
+
Home-page: https://github.com/jrschmidt2/webmo-python
|
|
6
|
+
Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.6.0.tar.gz
|
|
7
|
+
Author: J.R. Schmidt
|
|
8
|
+
Author-email: schmidt@webmo.net
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: WEBMO,REST,API
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Requires-Dist: websockets
|
|
14
|
+
Provides-Extra: spectrum
|
|
15
|
+
Requires-Dist: numpy; extra == "spectrum"
|
|
16
|
+
Provides-Extra: util
|
|
17
|
+
Requires-Dist: PubChemPy; extra == "util"
|
|
18
|
+
Provides-Extra: gui
|
|
19
|
+
Requires-Dist: questionary; extra == "gui"
|
|
20
|
+
Requires-Dist: ipywidgets; extra == "gui"
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: numpy; extra == "all"
|
|
23
|
+
Requires-Dist: PubChemPy; extra == "all"
|
|
24
|
+
Requires-Dist: questionary; extra == "all"
|
|
25
|
+
Requires-Dist: ipywidgets; extra == "all"
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: download-url
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: provides-extra
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: summary
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from .webmo_rest import WebMOREST
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.6.0"
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
|
|
3
|
-
def xyz_from_name(name):
|
|
3
|
+
def xyz_from_name(name, include_atomic_number=False):
|
|
4
4
|
"""Obtains an XYZ-formatted geometry for the molecule name via PubChem lookup.
|
|
5
5
|
|
|
6
6
|
Arguments:
|
|
7
7
|
name(str): The name of the molecule.
|
|
8
|
+
include_atomic_number(bool): Whether to include the atomic number as well in the XYZ output (e.g. for GAMESS formatted Cartesian coordinates)
|
|
8
9
|
|
|
9
10
|
Returns:
|
|
10
11
|
A string containing the molecule's XYZ-formatted geometry.
|
|
@@ -19,10 +20,14 @@ def xyz_from_name(name):
|
|
|
19
20
|
geom = ""
|
|
20
21
|
for atom in record.atoms:
|
|
21
22
|
element = atom.element
|
|
23
|
+
number = int(atom.number)
|
|
22
24
|
x = float(atom.x)
|
|
23
25
|
y = float(atom.y)
|
|
24
26
|
z = float(atom.z)
|
|
25
|
-
|
|
27
|
+
if include_atomic_number:
|
|
28
|
+
geom += f"{element}\t{number:d}\t{x:f}\t{y:f}\t{z:f}\n"
|
|
29
|
+
else:
|
|
30
|
+
geom += f"{element}\t{x:f}\t{y:f}\t{z:f}\n"
|
|
26
31
|
|
|
27
32
|
return geom
|
|
28
33
|
|
|
@@ -33,8 +38,8 @@ def get_energy(results, include_units=False):
|
|
|
33
38
|
Requires WebMO 25 or higher.
|
|
34
39
|
|
|
35
40
|
Arguments:
|
|
36
|
-
dict
|
|
37
|
-
bool
|
|
41
|
+
results(dict): A dictionary of job results, obtained from get_job_results.
|
|
42
|
+
include_units(bool): Whether to include the units with the returned energy (e.g. 1.5 vs. "1.5 Hartree")
|
|
38
43
|
|
|
39
44
|
Returns:
|
|
40
45
|
The molecular energy.
|
|
@@ -65,8 +70,8 @@ def get_energies(results, include_units=False):
|
|
|
65
70
|
"""Returns a dictionary of available energies from the job results.
|
|
66
71
|
|
|
67
72
|
Arguments:
|
|
68
|
-
dict
|
|
69
|
-
bool
|
|
73
|
+
results(dict): A dictionary of job results, obtained from get_job_results.
|
|
74
|
+
include_units(bool): Whether to include the units with the returned energy (e.g. 1.5 vs. "1.5 Hartree")
|
|
70
75
|
|
|
71
76
|
Returns:
|
|
72
77
|
A dictionary of available energies.
|
|
@@ -92,7 +97,7 @@ def get_geometry(results):
|
|
|
92
97
|
"""Returns the XYZ-formatted final geometry from the job results.
|
|
93
98
|
|
|
94
99
|
Arguments:
|
|
95
|
-
dict
|
|
100
|
+
results(dict): A dictionary of job results, obtained from get_job_results.
|
|
96
101
|
|
|
97
102
|
Returns:
|
|
98
103
|
The XYZ-formatted final geometry as a string.
|
|
@@ -116,7 +121,7 @@ def get_stoichiometry(results):
|
|
|
116
121
|
"""Returns the stoichiometry from the job results.
|
|
117
122
|
|
|
118
123
|
Arguments:
|
|
119
|
-
dict
|
|
124
|
+
results(dict): A dictionary of job results, obtained from get_job_results.
|
|
120
125
|
|
|
121
126
|
Returns:
|
|
122
127
|
The stoichiometry as a string.
|
|
@@ -129,7 +134,7 @@ def get_dipole_moment(results):
|
|
|
129
134
|
"""Returns the dipole moment from the job results.
|
|
130
135
|
|
|
131
136
|
Arguments:
|
|
132
|
-
dict
|
|
137
|
+
results(dict): A dictionary of job results, obtained from get_job_results.
|
|
133
138
|
|
|
134
139
|
Returns:
|
|
135
140
|
The magnitude of the dipole moment.
|
|
@@ -147,8 +152,8 @@ def get_property(results, property_name):
|
|
|
147
152
|
"""Returns an arbitrary calculated property from the job results.
|
|
148
153
|
|
|
149
154
|
Arguments:
|
|
150
|
-
dict
|
|
151
|
-
str
|
|
155
|
+
results(dict): A dictionary of job results, obtained from get_job_results.
|
|
156
|
+
property_name(str): The name of the desired property
|
|
152
157
|
|
|
153
158
|
Returns:
|
|
154
159
|
The value of the calculated property (scalar or dictionary).
|
|
@@ -266,6 +271,72 @@ def get_dihedral_angle(results, index1, index2, index3, index4):
|
|
|
266
271
|
|
|
267
272
|
return np.arctan2(y, x)*180./np.pi
|
|
268
273
|
|
|
274
|
+
def get_option_matching(template, variable, desired_value_aliases, ignore_missing_variables=False):
|
|
275
|
+
"""Returns the best matching template variable option for the specified variable.
|
|
276
|
+
|
|
277
|
+
This can be useful when an engine uses non-standard names for options and you want to use a
|
|
278
|
+
more descriptive name (like that shown in the dropdown) rather than the corresponding value. It attempts
|
|
279
|
+
to match either the corresponding option value (if it exists) OR the description.
|
|
280
|
+
|
|
281
|
+
For example:
|
|
282
|
+
get_option_matching(template, 'basisSet', '6-31G(d)')
|
|
283
|
+
will return the value:
|
|
284
|
+
"N31 NGAUSS=6 NDFUNC=1"
|
|
285
|
+
since it matches the corresponding option description "Routine: 6-31G(d)" in the dropdown.
|
|
286
|
+
|
|
287
|
+
Arguments:
|
|
288
|
+
template(dict): The desired job template (from get_templates)
|
|
289
|
+
variable(str): The template variable
|
|
290
|
+
desired_value_aliases(list): A list of potentially matching values, or aliases thereof; a single string is also acceptable.
|
|
291
|
+
ignore_missing_variables(bool, opt): Whether to raise an exception if the template variable does not exist in the template
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
The best matching corresponding variable value
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def ends_with_whole_word(text, target_word):
|
|
298
|
+
# Check if it ends with the word
|
|
299
|
+
if not text.endswith(target_word):
|
|
300
|
+
return False
|
|
301
|
+
# Get the start index of the target word in the text
|
|
302
|
+
idx = len(text) - len(target_word)
|
|
303
|
+
# If it's at the very beginning of the string, it's a whole word
|
|
304
|
+
if idx == 0:
|
|
305
|
+
return True
|
|
306
|
+
# Check if the character just before our word is a space or a colon
|
|
307
|
+
return text[idx - 1].isspace() or text[idx - 1] == ':'
|
|
308
|
+
|
|
309
|
+
#immediately return if the desired value is blank (for some reason)
|
|
310
|
+
if isinstance(desired_value_aliases, str) and not desired_value_aliases.strip():
|
|
311
|
+
return ""
|
|
312
|
+
|
|
313
|
+
#normalize any scalar arguement ot a list
|
|
314
|
+
if not isinstance(desired_value_aliases, list):
|
|
315
|
+
desired_value_aliases = [desired_value_aliases]
|
|
316
|
+
#immediately return if this variable does not exist in this particular template
|
|
317
|
+
if not variable in template['variables']:
|
|
318
|
+
if ignore_missing_variables:
|
|
319
|
+
return ""
|
|
320
|
+
else:
|
|
321
|
+
raise ValueError(f"Could not locate variable {variable} in template")
|
|
322
|
+
#ensure that the template variable is a 'dropdown' type
|
|
323
|
+
if template['variables'][variable]['type'] != 'dropdown':
|
|
324
|
+
raise ValueError(f"Template variable {variable} is not of type dropdown")
|
|
325
|
+
|
|
326
|
+
options = template['variables'][variable]['options']
|
|
327
|
+
for desired_value in desired_value_aliases:
|
|
328
|
+
for key, value in options.items():
|
|
329
|
+
#check if the specified value exists as value in the options dictionary
|
|
330
|
+
if value.casefold() == desired_value.casefold():
|
|
331
|
+
return value
|
|
332
|
+
#otherwise check if it matches the end of any key in the dictionary
|
|
333
|
+
if ends_with_whole_word(key.casefold(), desired_value.casefold()):
|
|
334
|
+
return options[key]
|
|
335
|
+
|
|
336
|
+
#not found!
|
|
337
|
+
raise ValueError(f"Could not locate option {desired_value_aliases} for template variable {variable}")
|
|
338
|
+
|
|
339
|
+
|
|
269
340
|
def _xyz_to_array(xyz):
|
|
270
341
|
xyz = xyz.strip()
|
|
271
342
|
|
|
@@ -284,4 +355,4 @@ def _xyz_to_array(xyz):
|
|
|
284
355
|
del geom[0::4]
|
|
285
356
|
|
|
286
357
|
#convert to an array of floats and return
|
|
287
|
-
return np.array(geom,dtype=float)
|
|
358
|
+
return np.array(geom,dtype=float)
|
|
@@ -16,30 +16,43 @@ class WebMOREST:
|
|
|
16
16
|
creation/destruction.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
def __init__(self, base_url, username, password="", expiration=24):
|
|
19
|
+
def __init__(self, base_url, username, password="", expiration=24, totp="", recovery_code=""):
|
|
20
20
|
"""Constructor for WebMOREST
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
This constructor generates a WebMOREST object and also generates and stores a newly
|
|
23
23
|
session token.
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
Args:
|
|
26
26
|
base_url(str): The base URL (ending in rest.cgi) of the WeBMO rest endpoint
|
|
27
27
|
username(str): The WebMO username
|
|
28
28
|
password(str, optional): The WebMO password; if omitted, this is supplied interactively
|
|
29
29
|
expiration(int, optional): The expiration time (in hours) of the REST session token
|
|
30
|
-
|
|
30
|
+
totp(str, optional): The current TOTP code from an authenticator app, for users with MFA enabled;
|
|
31
|
+
if omitted and MFA is required, a TOTP or recovery code is supplied interactively
|
|
32
|
+
recovery_code(str, optional): A single-use MFA recovery code, as an alternative to totp
|
|
33
|
+
|
|
31
34
|
Returns:
|
|
32
35
|
object: The newly constructed WebMO object
|
|
33
36
|
"""
|
|
34
37
|
from getpass import getpass
|
|
35
|
-
|
|
38
|
+
|
|
36
39
|
#prompt for WebMO password if not specified
|
|
37
40
|
if not password:
|
|
38
41
|
password=getpass(prompt="Enter WebMO password for user %s:" % username)
|
|
39
42
|
#obtain a REST token using the specified credentials
|
|
40
|
-
login={'username' : username, 'password' : password, 'expiration' : expiration
|
|
43
|
+
login={'username' : username, 'password' : password, 'expiration' : expiration,
|
|
44
|
+
'totp' : totp, 'recoveryCode' : recovery_code}
|
|
41
45
|
r = requests.post(base_url + '/sessions', data=login)
|
|
42
|
-
|
|
46
|
+
#prompt for an MFA code if required but not specified; a request without a code
|
|
47
|
+
#does not count as a failed MFA attempt, and a lockout is reported as an error below
|
|
48
|
+
if not totp and not recovery_code and WebMOREST._is_mfa_required(r):
|
|
49
|
+
code = input("Enter WebMO MFA code (or recovery code) for user %s:" % username).strip()
|
|
50
|
+
if code.isdigit():
|
|
51
|
+
login['totp'] = code
|
|
52
|
+
else:
|
|
53
|
+
login['recoveryCode'] = code
|
|
54
|
+
r = requests.post(base_url + '/sessions', data=login)
|
|
55
|
+
WebMOREST._check_status(r) #raise an exception if there is a problem with the request
|
|
43
56
|
|
|
44
57
|
self._base_url = base_url
|
|
45
58
|
self._auth=r.json() #save an authorization token need to authenticate future REST requests
|
|
@@ -53,14 +66,15 @@ class WebMOREST:
|
|
|
53
66
|
|
|
54
67
|
This destructor automatically deletes the session token using the REST interface
|
|
55
68
|
"""
|
|
56
|
-
#End the REST sessions; check in case requests has already been released
|
|
57
|
-
|
|
69
|
+
#End the REST sessions; check in case requests has already been released,
|
|
70
|
+
#or the constructor failed before a session token was obtained
|
|
71
|
+
if requests.delete is not None and hasattr(self, '_auth'):
|
|
58
72
|
r = requests.delete(self._base_url + '/sessions', params=self._auth)
|
|
59
73
|
#do not raise an exception for a failed request in this case due to issues
|
|
60
74
|
#with object management in Jupyter (i.e. on code re-run, a new token is made
|
|
61
75
|
#prior to deletion!)
|
|
62
76
|
|
|
63
|
-
if self
|
|
77
|
+
if getattr(self, '_callback_listener', None) is not None:
|
|
64
78
|
self._callback_listener.close()
|
|
65
79
|
|
|
66
80
|
#
|
|
@@ -78,7 +92,7 @@ class WebMOREST:
|
|
|
78
92
|
"""
|
|
79
93
|
|
|
80
94
|
r = requests.get(self._base_url + "/users", params=self._auth)
|
|
81
|
-
|
|
95
|
+
WebMOREST._check_status(r)
|
|
82
96
|
return r.json()["users"]
|
|
83
97
|
|
|
84
98
|
def get_user_info(self, username):
|
|
@@ -95,7 +109,7 @@ class WebMOREST:
|
|
|
95
109
|
"""
|
|
96
110
|
|
|
97
111
|
r = requests.get(self._base_url + "/users/%s" % username, params=self._auth)
|
|
98
|
-
|
|
112
|
+
WebMOREST._check_status(r)
|
|
99
113
|
return r.json()
|
|
100
114
|
|
|
101
115
|
#
|
|
@@ -112,7 +126,7 @@ class WebMOREST:
|
|
|
112
126
|
"""
|
|
113
127
|
|
|
114
128
|
r = requests.get(self._base_url + "/groups", params=self._auth)
|
|
115
|
-
|
|
129
|
+
WebMOREST._check_status(r)
|
|
116
130
|
return r.json()["groups"]
|
|
117
131
|
|
|
118
132
|
def get_group_info(self, groupname):
|
|
@@ -129,7 +143,7 @@ class WebMOREST:
|
|
|
129
143
|
"""
|
|
130
144
|
|
|
131
145
|
r = requests.get(self._base_url + "/groups/%s" % groupname, params=self._auth)
|
|
132
|
-
|
|
146
|
+
WebMOREST._check_status(r)
|
|
133
147
|
return r.json()
|
|
134
148
|
|
|
135
149
|
|
|
@@ -153,7 +167,7 @@ class WebMOREST:
|
|
|
153
167
|
params = self._auth.copy()
|
|
154
168
|
params.update({'user' : target_user})
|
|
155
169
|
r = requests.get(self._base_url + "/folders", params=params)
|
|
156
|
-
|
|
170
|
+
WebMOREST._check_status(r)
|
|
157
171
|
return r.json()["folders"]
|
|
158
172
|
|
|
159
173
|
#
|
|
@@ -179,7 +193,7 @@ class WebMOREST:
|
|
|
179
193
|
params = self._auth.copy()
|
|
180
194
|
params.update({'engine' : engine, 'status' : status, 'folderID' : folder_id, 'jobName' : job_name, 'user' : target_user})
|
|
181
195
|
r = requests.get(self._base_url + '/jobs', params=params)
|
|
182
|
-
|
|
196
|
+
WebMOREST._check_status(r)
|
|
183
197
|
return r.json()["jobs"]
|
|
184
198
|
|
|
185
199
|
def get_job_info(self, job_number):
|
|
@@ -195,7 +209,7 @@ class WebMOREST:
|
|
|
195
209
|
"""
|
|
196
210
|
|
|
197
211
|
r = requests.get(self._base_url + "/jobs/%d" % job_number, params=self._auth)
|
|
198
|
-
|
|
212
|
+
WebMOREST._check_status(r)
|
|
199
213
|
return r.json()
|
|
200
214
|
|
|
201
215
|
def _check_valid_job_list(self, job_numbers):
|
|
@@ -227,7 +241,7 @@ class WebMOREST:
|
|
|
227
241
|
params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
|
|
228
242
|
r = requests.get(self._base_url + "/jobs/results", params=params)
|
|
229
243
|
|
|
230
|
-
|
|
244
|
+
WebMOREST._check_status(r)
|
|
231
245
|
return r.json()
|
|
232
246
|
|
|
233
247
|
def get_job_geometry(self, job_number):
|
|
@@ -243,7 +257,7 @@ class WebMOREST:
|
|
|
243
257
|
"""
|
|
244
258
|
|
|
245
259
|
r = requests.get(self._base_url + "/jobs/%d/geometry" % job_number, params=self._auth)
|
|
246
|
-
|
|
260
|
+
WebMOREST._check_status(r)
|
|
247
261
|
return r.json()["xyz"]
|
|
248
262
|
|
|
249
263
|
def get_job_output(self, job_number):
|
|
@@ -259,7 +273,7 @@ class WebMOREST:
|
|
|
259
273
|
"""
|
|
260
274
|
|
|
261
275
|
r = requests.get(self._base_url + "/jobs/%d/raw_output" % job_number, params=self._auth)
|
|
262
|
-
|
|
276
|
+
WebMOREST._check_status(r)
|
|
263
277
|
return r.text
|
|
264
278
|
|
|
265
279
|
def get_job_archive(self, *job_numbers):
|
|
@@ -280,7 +294,7 @@ class WebMOREST:
|
|
|
280
294
|
params = self._auth.copy()
|
|
281
295
|
params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
|
|
282
296
|
r = requests.get(self._base_url + "/jobs/archive" % (), params=params)
|
|
283
|
-
|
|
297
|
+
WebMOREST._check_status(r)
|
|
284
298
|
return r.text
|
|
285
299
|
|
|
286
300
|
def get_job_spreadsheet(self, *job_numbers):
|
|
@@ -301,7 +315,7 @@ class WebMOREST:
|
|
|
301
315
|
params = self._auth.copy()
|
|
302
316
|
params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
|
|
303
317
|
r = requests.get(self._base_url + "/jobs/spreadsheet" % (), params=params)
|
|
304
|
-
|
|
318
|
+
WebMOREST._check_status(r)
|
|
305
319
|
return r.text
|
|
306
320
|
|
|
307
321
|
def delete_job(self, job_number):
|
|
@@ -314,7 +328,7 @@ class WebMOREST:
|
|
|
314
328
|
"""
|
|
315
329
|
|
|
316
330
|
r = requests.delete(self._base_url + "/jobs/%d" % job_number, params=self._auth)
|
|
317
|
-
|
|
331
|
+
WebMOREST._check_status(r)
|
|
318
332
|
|
|
319
333
|
def import_job(self, job_name, output_filename, engine):
|
|
320
334
|
"""Imports an existing output file into WebMO
|
|
@@ -335,7 +349,7 @@ class WebMOREST:
|
|
|
335
349
|
params.update({'jobName' : job_name, 'engine' : engine})
|
|
336
350
|
output_file = {'outputFile' : ('output.log', open(output_filename, 'rb'), 'text/plain')}
|
|
337
351
|
r = requests.post(self._base_url + '/jobs', data=params, files=output_file)
|
|
338
|
-
|
|
352
|
+
WebMOREST._check_status(r)
|
|
339
353
|
return r.json()["jobNumber"]
|
|
340
354
|
|
|
341
355
|
def submit_job(self, job_name, input_contents, engine, queue=None, nodes=1, ppn=1, **kwargs):
|
|
@@ -362,7 +376,7 @@ class WebMOREST:
|
|
|
362
376
|
params.update({'jobName' : job_name, 'engine' : engine, 'inputFile': input_contents,
|
|
363
377
|
'queue': queue, 'nodes': nodes, 'ppn': ppn, **kwargs})
|
|
364
378
|
r = requests.post(self._base_url + '/jobs', data=params)
|
|
365
|
-
|
|
379
|
+
WebMOREST._check_status(r)
|
|
366
380
|
return r.json()["jobNumber"]
|
|
367
381
|
|
|
368
382
|
#
|
|
@@ -381,7 +395,7 @@ class WebMOREST:
|
|
|
381
395
|
"""
|
|
382
396
|
|
|
383
397
|
r = requests.get(self._base_url + "/templates/%s" % engine, params=self._auth)
|
|
384
|
-
|
|
398
|
+
WebMOREST._check_status(r)
|
|
385
399
|
return r.json()["templates"]
|
|
386
400
|
|
|
387
401
|
|
|
@@ -399,6 +413,12 @@ class WebMOREST:
|
|
|
399
413
|
A text-formatted input file
|
|
400
414
|
"""
|
|
401
415
|
amend_variables = variables.copy()
|
|
416
|
+
#handle booleans, converting False to '' if the template variable is a checkbox
|
|
417
|
+
for var in amend_variables:
|
|
418
|
+
if isinstance(amend_variables[var], bool) and (var in template['variables']) and (template['variables'][var]['type'] == 'checkbox'):
|
|
419
|
+
if amend_variables[var] == False:
|
|
420
|
+
amend_variables[var] = ''
|
|
421
|
+
#add in defaults, if appropriate
|
|
402
422
|
if auto_defaults:
|
|
403
423
|
#update and amend 'variables' with default values for missing template parameters
|
|
404
424
|
for var in template['variables']:
|
|
@@ -409,7 +429,7 @@ class WebMOREST:
|
|
|
409
429
|
params = self._auth.copy()
|
|
410
430
|
params.update({'variables' : json.dumps(amend_variables)})
|
|
411
431
|
r = requests.get(self._base_url + "/templates/%s" % template['id'], params=params)
|
|
412
|
-
|
|
432
|
+
WebMOREST._check_status(r)
|
|
413
433
|
return r.text
|
|
414
434
|
|
|
415
435
|
#
|
|
@@ -432,7 +452,7 @@ class WebMOREST:
|
|
|
432
452
|
params = self._auth.copy()
|
|
433
453
|
params.update({'user' : target_user})
|
|
434
454
|
r = requests.get(self._base_url + "/engines", params=params)
|
|
435
|
-
|
|
455
|
+
WebMOREST._check_status(r)
|
|
436
456
|
return r.json()["engines"]
|
|
437
457
|
|
|
438
458
|
#
|
|
@@ -479,7 +499,7 @@ class WebMOREST:
|
|
|
479
499
|
await self._create_callback_listener()
|
|
480
500
|
|
|
481
501
|
r = requests.get(self._base_url + "/jobs/%d/geometry" % job_number, params=self._auth)
|
|
482
|
-
|
|
502
|
+
WebMOREST._check_status(r)
|
|
483
503
|
geometryJSON = json.dumps(r.json())
|
|
484
504
|
geometryJSON = geometryJSON.replace("\\", "\\\\")
|
|
485
505
|
|
|
@@ -614,7 +634,7 @@ class WebMOREST:
|
|
|
614
634
|
"""
|
|
615
635
|
|
|
616
636
|
r = requests.get(self._base_url + "/status", params=self._auth)
|
|
617
|
-
|
|
637
|
+
WebMOREST._check_status(r)
|
|
618
638
|
return r.json()
|
|
619
639
|
|
|
620
640
|
|
|
@@ -651,7 +671,29 @@ class WebMOREST:
|
|
|
651
671
|
#
|
|
652
672
|
# Private helper methods
|
|
653
673
|
#
|
|
654
|
-
|
|
674
|
+
@staticmethod
|
|
675
|
+
def _check_status(response):
|
|
676
|
+
try:
|
|
677
|
+
response.raise_for_status()
|
|
678
|
+
except requests.exceptions.HTTPError as e:
|
|
679
|
+
# Grab the original message and append the body
|
|
680
|
+
new_message = f"{e.args[0]}\nResponse Body: {response.text[:500]}"
|
|
681
|
+
# Overwrite the exception arguments with your new message
|
|
682
|
+
e.args = (new_message,)
|
|
683
|
+
raise e
|
|
684
|
+
|
|
685
|
+
@staticmethod
|
|
686
|
+
def _is_mfa_required(response):
|
|
687
|
+
#MFA-enabled users without a valid code receive a 401 flagged with mfaRequired;
|
|
688
|
+
#an MFA lockout is flagged the same way, but prompting for a code would not help
|
|
689
|
+
if response.status_code != 401:
|
|
690
|
+
return False
|
|
691
|
+
try:
|
|
692
|
+
body = response.json()
|
|
693
|
+
except ValueError:
|
|
694
|
+
return False
|
|
695
|
+
return body.get('mfaRequired', False) and body.get('error') == 'MFA code required'
|
|
696
|
+
|
|
655
697
|
def _inject_javascript(self):
|
|
656
698
|
if self._init_javascript:
|
|
657
699
|
config = self.get_status_info()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webmo
|
|
3
|
+
Version: 1.6.0
|
|
4
|
+
Summary: A Python-based interface to WebMO
|
|
5
|
+
Home-page: https://github.com/jrschmidt2/webmo-python
|
|
6
|
+
Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.6.0.tar.gz
|
|
7
|
+
Author: J.R. Schmidt
|
|
8
|
+
Author-email: schmidt@webmo.net
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: WEBMO,REST,API
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Requires-Dist: websockets
|
|
14
|
+
Provides-Extra: spectrum
|
|
15
|
+
Requires-Dist: numpy; extra == "spectrum"
|
|
16
|
+
Provides-Extra: util
|
|
17
|
+
Requires-Dist: PubChemPy; extra == "util"
|
|
18
|
+
Provides-Extra: gui
|
|
19
|
+
Requires-Dist: questionary; extra == "gui"
|
|
20
|
+
Requires-Dist: ipywidgets; extra == "gui"
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: numpy; extra == "all"
|
|
23
|
+
Requires-Dist: PubChemPy; extra == "all"
|
|
24
|
+
Requires-Dist: questionary; extra == "all"
|
|
25
|
+
Requires-Dist: ipywidgets; extra == "all"
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: download-url
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: provides-extra
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: summary
|
webmo-1.4.2/PKG-INFO
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: webmo
|
|
3
|
-
Version: 1.4.2
|
|
4
|
-
Summary: A Python-based interface to WebMO
|
|
5
|
-
Home-page: https://github.com/jrschmidt2/webmo-python
|
|
6
|
-
Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.4.2.tar.gz
|
|
7
|
-
Author: J.R. Schmidt
|
|
8
|
-
Author-email: schmidt@webmo.net
|
|
9
|
-
License: MIT
|
|
10
|
-
Keywords: WEBMO,REST,API
|
|
11
|
-
Provides-Extra: spectrum
|
|
12
|
-
Provides-Extra: util
|
|
13
|
-
Provides-Extra: gui
|
|
14
|
-
Provides-Extra: all
|
|
15
|
-
License-File: LICENSE
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: webmo
|
|
3
|
-
Version: 1.4.2
|
|
4
|
-
Summary: A Python-based interface to WebMO
|
|
5
|
-
Home-page: https://github.com/jrschmidt2/webmo-python
|
|
6
|
-
Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.4.2.tar.gz
|
|
7
|
-
Author: J.R. Schmidt
|
|
8
|
-
Author-email: schmidt@webmo.net
|
|
9
|
-
License: MIT
|
|
10
|
-
Keywords: WEBMO,REST,API
|
|
11
|
-
Provides-Extra: spectrum
|
|
12
|
-
Provides-Extra: util
|
|
13
|
-
Provides-Extra: gui
|
|
14
|
-
Provides-Extra: all
|
|
15
|
-
License-File: LICENSE
|
|
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
|