webmo 1.4.2__tar.gz → 1.5.1__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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: webmo
3
- Version: 1.4.2
3
+ Version: 1.5.1
4
4
  Summary: A Python-based interface to WebMO
5
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
6
+ Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.5.1.tar.gz
7
7
  Author: J.R. Schmidt
8
8
  Author-email: schmidt@webmo.net
9
9
  License: MIT
@@ -1,2 +1,2 @@
1
1
  from .webmo_rest import WebMOREST
2
- __version__ = "1.4.2"
2
+ __version__ = "1.5.1"
@@ -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
- geom += f"{element}\t{x:f}\t{y:f}\t{z:f}\n"
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(results): A dictionary of job results, obtained from get_job_results.
37
- bool(include_units): Whether to include the units with the returned energy (e.g. 1.5 vs. "1.5 Hartree")
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(results): A dictionary of job results, obtained from get_job_results.
69
- bool(include_units): Whether to include the units with the returned energy (e.g. 1.5 vs. "1.5 Hartree")
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(results): A dictionary of job results, obtained from get_job_results.
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(results): A dictionary of job results, obtained from get_job_results.
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(results): A dictionary of job results, obtained from get_job_results.
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(results): A dictionary of job results, obtained from get_job_results.
151
- str(property_name): The name of the desired property
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)
@@ -39,7 +39,7 @@ class WebMOREST:
39
39
  #obtain a REST token using the specified credentials
40
40
  login={'username' : username, 'password' : password, 'expiration' : expiration}
41
41
  r = requests.post(base_url + '/sessions', data=login)
42
- r.raise_for_status() #raise an exception if there is a problem with the request
42
+ WebMOREST._check_status(r) #raise an exception if there is a problem with the request
43
43
 
44
44
  self._base_url = base_url
45
45
  self._auth=r.json() #save an authorization token need to authenticate future REST requests
@@ -78,7 +78,7 @@ class WebMOREST:
78
78
  """
79
79
 
80
80
  r = requests.get(self._base_url + "/users", params=self._auth)
81
- r.raise_for_status()
81
+ WebMOREST._check_status(r)
82
82
  return r.json()["users"]
83
83
 
84
84
  def get_user_info(self, username):
@@ -95,7 +95,7 @@ class WebMOREST:
95
95
  """
96
96
 
97
97
  r = requests.get(self._base_url + "/users/%s" % username, params=self._auth)
98
- r.raise_for_status()
98
+ WebMOREST._check_status(r)
99
99
  return r.json()
100
100
 
101
101
  #
@@ -112,7 +112,7 @@ class WebMOREST:
112
112
  """
113
113
 
114
114
  r = requests.get(self._base_url + "/groups", params=self._auth)
115
- r.raise_for_status()
115
+ WebMOREST._check_status(r)
116
116
  return r.json()["groups"]
117
117
 
118
118
  def get_group_info(self, groupname):
@@ -129,7 +129,7 @@ class WebMOREST:
129
129
  """
130
130
 
131
131
  r = requests.get(self._base_url + "/groups/%s" % groupname, params=self._auth)
132
- r.raise_for_status()
132
+ WebMOREST._check_status(r)
133
133
  return r.json()
134
134
 
135
135
 
@@ -153,7 +153,7 @@ class WebMOREST:
153
153
  params = self._auth.copy()
154
154
  params.update({'user' : target_user})
155
155
  r = requests.get(self._base_url + "/folders", params=params)
156
- r.raise_for_status()
156
+ WebMOREST._check_status(r)
157
157
  return r.json()["folders"]
158
158
 
159
159
  #
@@ -179,7 +179,7 @@ class WebMOREST:
179
179
  params = self._auth.copy()
180
180
  params.update({'engine' : engine, 'status' : status, 'folderID' : folder_id, 'jobName' : job_name, 'user' : target_user})
181
181
  r = requests.get(self._base_url + '/jobs', params=params)
182
- r.raise_for_status()
182
+ WebMOREST._check_status(r)
183
183
  return r.json()["jobs"]
184
184
 
185
185
  def get_job_info(self, job_number):
@@ -195,7 +195,7 @@ class WebMOREST:
195
195
  """
196
196
 
197
197
  r = requests.get(self._base_url + "/jobs/%d" % job_number, params=self._auth)
198
- r.raise_for_status()
198
+ WebMOREST._check_status(r)
199
199
  return r.json()
200
200
 
201
201
  def _check_valid_job_list(self, job_numbers):
@@ -227,7 +227,7 @@ class WebMOREST:
227
227
  params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
228
228
  r = requests.get(self._base_url + "/jobs/results", params=params)
229
229
 
230
- r.raise_for_status()
230
+ WebMOREST._check_status(r)
231
231
  return r.json()
232
232
 
233
233
  def get_job_geometry(self, job_number):
@@ -243,7 +243,7 @@ class WebMOREST:
243
243
  """
244
244
 
245
245
  r = requests.get(self._base_url + "/jobs/%d/geometry" % job_number, params=self._auth)
246
- r.raise_for_status()
246
+ WebMOREST._check_status(r)
247
247
  return r.json()["xyz"]
248
248
 
249
249
  def get_job_output(self, job_number):
@@ -259,7 +259,7 @@ class WebMOREST:
259
259
  """
260
260
 
261
261
  r = requests.get(self._base_url + "/jobs/%d/raw_output" % job_number, params=self._auth)
262
- r.raise_for_status()
262
+ WebMOREST._check_status(r)
263
263
  return r.text
264
264
 
265
265
  def get_job_archive(self, *job_numbers):
@@ -280,7 +280,7 @@ class WebMOREST:
280
280
  params = self._auth.copy()
281
281
  params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
282
282
  r = requests.get(self._base_url + "/jobs/archive" % (), params=params)
283
- r.raise_for_status()
283
+ WebMOREST._check_status(r)
284
284
  return r.text
285
285
 
286
286
  def get_job_spreadsheet(self, *job_numbers):
@@ -301,7 +301,7 @@ class WebMOREST:
301
301
  params = self._auth.copy()
302
302
  params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
303
303
  r = requests.get(self._base_url + "/jobs/spreadsheet" % (), params=params)
304
- r.raise_for_status()
304
+ WebMOREST._check_status(r)
305
305
  return r.text
306
306
 
307
307
  def delete_job(self, job_number):
@@ -314,7 +314,7 @@ class WebMOREST:
314
314
  """
315
315
 
316
316
  r = requests.delete(self._base_url + "/jobs/%d" % job_number, params=self._auth)
317
- r.raise_for_status()
317
+ WebMOREST._check_status(r)
318
318
 
319
319
  def import_job(self, job_name, output_filename, engine):
320
320
  """Imports an existing output file into WebMO
@@ -335,7 +335,7 @@ class WebMOREST:
335
335
  params.update({'jobName' : job_name, 'engine' : engine})
336
336
  output_file = {'outputFile' : ('output.log', open(output_filename, 'rb'), 'text/plain')}
337
337
  r = requests.post(self._base_url + '/jobs', data=params, files=output_file)
338
- r.raise_for_status()
338
+ WebMOREST._check_status(r)
339
339
  return r.json()["jobNumber"]
340
340
 
341
341
  def submit_job(self, job_name, input_contents, engine, queue=None, nodes=1, ppn=1, **kwargs):
@@ -362,7 +362,7 @@ class WebMOREST:
362
362
  params.update({'jobName' : job_name, 'engine' : engine, 'inputFile': input_contents,
363
363
  'queue': queue, 'nodes': nodes, 'ppn': ppn, **kwargs})
364
364
  r = requests.post(self._base_url + '/jobs', data=params)
365
- r.raise_for_status()
365
+ WebMOREST._check_status(r)
366
366
  return r.json()["jobNumber"]
367
367
 
368
368
  #
@@ -381,7 +381,7 @@ class WebMOREST:
381
381
  """
382
382
 
383
383
  r = requests.get(self._base_url + "/templates/%s" % engine, params=self._auth)
384
- r.raise_for_status()
384
+ WebMOREST._check_status(r)
385
385
  return r.json()["templates"]
386
386
 
387
387
 
@@ -399,6 +399,12 @@ class WebMOREST:
399
399
  A text-formatted input file
400
400
  """
401
401
  amend_variables = variables.copy()
402
+ #handle booleans, converting False to '' if the template variable is a checkbox
403
+ for var in amend_variables:
404
+ if isinstance(amend_variables[var], bool) and (var in template['variables']) and (template['variables'][var]['type'] == 'checkbox'):
405
+ if amend_variables[var] == False:
406
+ amend_variables[var] = ''
407
+ #add in defaults, if appropriate
402
408
  if auto_defaults:
403
409
  #update and amend 'variables' with default values for missing template parameters
404
410
  for var in template['variables']:
@@ -409,7 +415,7 @@ class WebMOREST:
409
415
  params = self._auth.copy()
410
416
  params.update({'variables' : json.dumps(amend_variables)})
411
417
  r = requests.get(self._base_url + "/templates/%s" % template['id'], params=params)
412
- r.raise_for_status()
418
+ WebMOREST._check_status(r)
413
419
  return r.text
414
420
 
415
421
  #
@@ -432,7 +438,7 @@ class WebMOREST:
432
438
  params = self._auth.copy()
433
439
  params.update({'user' : target_user})
434
440
  r = requests.get(self._base_url + "/engines", params=params)
435
- r.raise_for_status()
441
+ WebMOREST._check_status(r)
436
442
  return r.json()["engines"]
437
443
 
438
444
  #
@@ -479,7 +485,7 @@ class WebMOREST:
479
485
  await self._create_callback_listener()
480
486
 
481
487
  r = requests.get(self._base_url + "/jobs/%d/geometry" % job_number, params=self._auth)
482
- r.raise_for_status()
488
+ WebMOREST._check_status(r)
483
489
  geometryJSON = json.dumps(r.json())
484
490
  geometryJSON = geometryJSON.replace("\\", "\\\\")
485
491
 
@@ -614,7 +620,7 @@ class WebMOREST:
614
620
  """
615
621
 
616
622
  r = requests.get(self._base_url + "/status", params=self._auth)
617
- r.raise_for_status()
623
+ WebMOREST._check_status(r)
618
624
  return r.json()
619
625
 
620
626
 
@@ -651,6 +657,16 @@ class WebMOREST:
651
657
  #
652
658
  # Private helper methods
653
659
  #
660
+ @staticmethod
661
+ def _check_status(response):
662
+ try:
663
+ response.raise_for_status()
664
+ except requests.exceptions.HTTPError as e:
665
+ # Grab the original message and append the body
666
+ new_message = f"{e.args[0]}\nResponse Body: {response.text[:500]}"
667
+ # Overwrite the exception arguments with your new message
668
+ e.args = (new_message,)
669
+ raise e
654
670
 
655
671
  def _inject_javascript(self):
656
672
  if self._init_javascript:
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: webmo
3
- Version: 1.4.2
3
+ Version: 1.5.1
4
4
  Summary: A Python-based interface to WebMO
5
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
6
+ Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.5.1.tar.gz
7
7
  Author: J.R. Schmidt
8
8
  Author-email: schmidt@webmo.net
9
9
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes