webmo 1.2.0__tar.gz → 1.3.3__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,12 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: webmo
3
- Version: 1.2.0
3
+ Version: 1.3.3
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.2.0.tar.gz
6
+ Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.3.3.tar.gz
7
7
  Author: J.R. Schmidt
8
8
  Author-email: schmidt@webmo.net
9
9
  License: MIT
10
10
  Keywords: WEBMO,REST,API
11
11
  Provides-Extra: spectrum
12
+ Provides-Extra: util
13
+ Provides-Extra: gui
14
+ Provides-Extra: all
12
15
  License-File: LICENSE
@@ -33,6 +33,9 @@ setup(
33
33
  'websockets',
34
34
  ],
35
35
  extras_require={
36
- "spectrum": ["numpy"]
36
+ "spectrum": ["numpy"],
37
+ "util": ["PubChemPy"],
38
+ "gui": ["questionary", "ipywidgets"],
39
+ "all": ["numpy", "PubChemPy", "questionary", "ipywidgets"]
37
40
  }
38
41
  )
@@ -1,2 +1,2 @@
1
1
  from .webmo_rest import WebMOREST
2
- __version__ = "1.2.0"
2
+ __version__ = "1.3.3"
@@ -0,0 +1,181 @@
1
+ #handle abstract base class
2
+ from abc import ABC, abstractmethod
3
+
4
+ class _WebMOGUIBase(ABC):
5
+ """The _WebMOGUIBase class is an abstract base class for all Python-based GUIs for WebMO.
6
+ """
7
+
8
+ def __init__(self, template, query_vars, additional_vars):
9
+ self._query_vars = query_vars
10
+ self._additional_vars = additional_vars
11
+
12
+ @abstractmethod
13
+ def display(self):
14
+ """Presents the menu-driven JupyterGUI
15
+
16
+ Displays the JupyterGUI using a system of IPython widgets.
17
+
18
+ Args:
19
+ None
20
+
21
+ Returns:
22
+ None
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ def get_variables(self):
28
+ """Returns a populated variables dictionary.
29
+
30
+ Returns a populated variables dictionary, generated using the user-selected
31
+ menu options, along with an additional user-specified key/value pairs (additional_vars)
32
+
33
+ Args:
34
+ None
35
+
36
+ Returns:
37
+ A populated dictionary of of variables and values.
38
+ """
39
+ pass
40
+
41
+ class JupyterGUI(_WebMOGUIBase):
42
+ """The JupyterGUI class is a menu-driven Jupyter GUI interface for setting up WebMO jobs.
43
+
44
+ The class utilizes IPython widgets to present a menu-driven interface, using a subset
45
+ of user-specified list of template variables.
46
+ """
47
+ try:
48
+ import ipywidgets
49
+ except:
50
+ #fail silently, since this should never happen in Jupyter
51
+ pass #print('ipywidgets not found; disabling support for JupyterGUI')
52
+
53
+ def __init__(self, template, query_vars, additional_vars=None):
54
+ """Constructor for JupyterGUI
55
+
56
+ This constructor generates a JupyterGUI object.
57
+
58
+ Args:
59
+ template(dict): A specific template, selected from WebMOREST.get_templates()
60
+ query_vars(list): A subset of variables to use when generating the GUI
61
+ additional_vars(dict, optional): A dictionary of additional variables/values to utilize during input file generation
62
+
63
+ Returns:
64
+ object: The newly constructed JupyterGUI object
65
+ """
66
+
67
+ _WebMOGUIBase.__init__(self, template, query_vars, additional_vars)
68
+ self._widgets = []
69
+
70
+ for var in query_vars:
71
+ if template['variables'][var]['type'] == 'checkbox':
72
+ #handle checkbox options
73
+ self._widgets.append(
74
+ self.ipywidgets.widgets.Checkbox(
75
+ value=True if template['variables'][var]['default'] == 'on' else False,
76
+ description=var,
77
+ disabled=False));
78
+ elif template['variables'][var]['type'] == 'dropdown':
79
+ #handle select/dropdown options
80
+ self._widgets.append(
81
+ self.ipywidgets.widgets.Dropdown(
82
+ options=template['variables'][var]['options'],
83
+ value=template['variables'][var]['default'],
84
+ description=var,
85
+ disabled=False));
86
+ else:
87
+ #handle text input
88
+ self._widgets.append(
89
+ self.ipywidgets.widgets.Text(
90
+ description=var,
91
+ value=template['variables'][var]['default'],
92
+ disabled=False));
93
+
94
+ def display(self):
95
+
96
+ for widget in self._widgets:
97
+ display(widget)
98
+
99
+ def get_variables(self):
100
+
101
+ # add in additional user-specified variables
102
+ if (self._additional_vars != None):
103
+ variables = {**self._additional_vars}
104
+ else:
105
+ variables = {}
106
+
107
+ for (var, widget) in zip(self._query_vars, self._widgets):
108
+ if isinstance(widget, self.ipywidgets.Checkbox):
109
+ variables[var] = "on" if widget.value == True else ""
110
+ else:
111
+ variables[var] = widget.value
112
+
113
+ return variables
114
+
115
+ class ConsoleGUI(_WebMOGUIBase):
116
+ """The ConsoleGUI class is a text-based interface for setting up WebMO jobs.
117
+
118
+ The class utilizes the questionary library to present a neat interface using a subset
119
+ of user-specified list of template variables.
120
+ """
121
+
122
+ def __init__(self, template, query_vars, additional_vars=None):
123
+ """Constructor for ConsoleGUI
124
+
125
+ This constructor generates a ConsoleGUI object.
126
+
127
+ Args:
128
+ template(dict): A specific template, selected from WebMOREST.get_templates()
129
+ query_vars(list): A subset of variables to use when generating the GUI
130
+ additional_vars(dict, optional): A dictionary of additional variables/values to utilize during input file generation
131
+
132
+ Returns:
133
+ object: The newly constructed ConsoleGUI object
134
+ """
135
+
136
+ import questionary as q
137
+
138
+ _WebMOGUIBase.__init__(self, template, query_vars, additional_vars)
139
+ self._questions = []
140
+ for var in query_vars:
141
+ # checkbox options
142
+ if template['variables'][var]['type'] == 'checkbox':
143
+ self._questions.append(q.confirm(var));
144
+ # select/dropdown options
145
+ elif template['variables'][var]['type'] == 'dropdown':
146
+ choices = []
147
+ for key, value in template['variables'][var]['options'].items():
148
+ choices.append(q.Choice(title=key, value=value))
149
+
150
+ self._questions.append(
151
+ q.select(
152
+ var,
153
+ choices=choices,
154
+ use_shortcuts=True));
155
+ # text input
156
+ else:
157
+ self._questions.append(
158
+ q.text(var));
159
+
160
+
161
+ def display(self):
162
+
163
+ self._answers = []
164
+ for question in self._questions:
165
+ self._answers.append(question.ask())
166
+
167
+ def get_variables(self):
168
+
169
+ # add in additional user-specified variables
170
+ if (self._additional_vars != None):
171
+ variables = {**self._additional_vars}
172
+ else:
173
+ variables = {}
174
+
175
+ for (var, value) in zip(self._query_vars, self._answers):
176
+ if isinstance(value, bool):
177
+ variables[var] = "on" if value else ""
178
+ else:
179
+ variables[var] = value
180
+
181
+ return variables
@@ -0,0 +1,25 @@
1
+ def xyz_from_name(name):
2
+ """Obtains an XYZ-formatted geometry for the molecule name via PubChem lookup.
3
+
4
+ Arguments:
5
+ name(str): The name of the molecule.
6
+
7
+ Returns:
8
+ A string containing the molecule's XYZ-formatted geometry.
9
+ """
10
+ import pubchempy as pcp
11
+
12
+ results = pcp.get_compounds(name, 'name', record_type='3d')
13
+ if (len(results) == 0):
14
+ raise ValueError("Specified molecule could not be found in PubChem")
15
+
16
+ record = results[0]
17
+ geom = ""
18
+ for atom in record.atoms:
19
+ element = atom.element
20
+ x = float(atom.x)
21
+ y = float(atom.y)
22
+ z = float(atom.z)
23
+ geom += f"{element}\t{x:f}\t{y:f}\t{z:f}\n"
24
+
25
+ return geom
@@ -16,7 +16,7 @@ class WebMOREST:
16
16
  creation/destruction.
17
17
  """
18
18
 
19
- def __init__(self, base_url, username, password=""):
19
+ def __init__(self, base_url, username, password="", expiration=24):
20
20
  """Constructor for WebMOREST
21
21
 
22
22
  This constructor generates a WebMOREST object and also generates and stores a newly
@@ -26,6 +26,7 @@ class WebMOREST:
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
+ expiration(int, optional): The expiration time (in hours) of the REST session token
29
30
 
30
31
  Returns:
31
32
  object: The newly constructed WebMO object
@@ -36,7 +37,7 @@ class WebMOREST:
36
37
  if not password:
37
38
  password=getpass(prompt="Enter WebMO password for user %s:" % username)
38
39
  #obtain a REST token using the specified credentials
39
- login={'username' : username, 'password' : password} #WebMO login information, used to retrieve a REST access token
40
+ login={'username' : username, 'password' : password, 'expiration' : expiration}
40
41
  r = requests.post(base_url + '/sessions', data=login)
41
42
  r.raise_for_status() #raise an exception if there is a problem with the request
42
43
 
@@ -52,8 +53,9 @@ class WebMOREST:
52
53
 
53
54
  This destructor automatically deletes the session token using the REST interface
54
55
  """
55
- #End the REST sessions
56
- r = requests.delete(self._base_url + '/sessions', params=self._auth)
56
+ #End the REST sessions; check in case requests has already been released
57
+ if requests.delete is not None:
58
+ r = requests.delete(self._base_url + '/sessions', params=self._auth)
57
59
  #do not raise an exception for a failed request in this case due to issues
58
60
  #with object management in Jupyter (i.e. on code re-run, a new token is made
59
61
  #prior to deletion!)
@@ -147,7 +149,7 @@ class WebMOREST:
147
149
  A list of folders
148
150
  """
149
151
 
150
- #append other relevant paramters
152
+ #append other relevant parameters
151
153
  params = self._auth.copy()
152
154
  params.update({'user' : target_user})
153
155
  r = requests.get(self._base_url + "/folders", params=params)
@@ -195,21 +197,36 @@ class WebMOREST:
195
197
  r = requests.get(self._base_url + "/jobs/%d" % job_number, params=self._auth)
196
198
  r.raise_for_status()
197
199
  return r.json()
198
-
199
- def get_job_results(self, job_number):
200
- """Returns detailed results of the calculation (e.g. energy, properties) from the specified job.
200
+
201
+ def _check_valid_job_list(self, job_numbers):
202
+ if len(job_numbers) == 0:
203
+ raise ValueError("One or more job number(s) must be specified")
204
+
205
+ def get_job_results(self, *job_numbers):
206
+ """Returns detailed results of the calculation (e.g. energy, properties) from the specified job(s).
201
207
 
202
208
  This call returns a JSON formatted string summarize all of the calculated and parsed properties
203
209
  from the specified job. This information is normally summarized on the View Job page.
204
210
 
205
211
  Arguments:
206
- job_number(int): The job about whom to return information
212
+ *job_numbers(int): The job(s) for which to generate the results
207
213
 
208
214
  Returns:
209
215
  A JSON formatted string summarizing the calculated properties
210
216
  """
211
-
212
- r = requests.get(self._base_url + "/jobs/%d/results" % job_number, params=self._auth)
217
+
218
+ self._check_valid_job_list(job_numbers)
219
+
220
+ if (len(job_numbers) == 1):
221
+ #handle case of single job, return JSON dictionary for single job
222
+ r = requests.get(self._base_url + "/jobs/%d/results" % job_numbers[0], params=self._auth)
223
+ else:
224
+ #handle case of multiple jobs, return JSON array of dictionaries
225
+ #append other relevant parameters
226
+ params = self._auth.copy()
227
+ params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
228
+ r = requests.get(self._base_url + "/jobs/results", params=params)
229
+
213
230
  r.raise_for_status()
214
231
  return r.json()
215
232
 
@@ -245,19 +262,45 @@ class WebMOREST:
245
262
  r.raise_for_status()
246
263
  return r.text
247
264
 
248
- def get_job_archive(self, job_number):
249
- """Returns a WebMO archive from the specified job.
265
+ def get_job_archive(self, *job_numbers):
266
+ """Returns a WebMO archive from the specified job(s).
250
267
 
251
- This call generates and returns a binary WebMO archive (tar/zip) file from the specified job.
268
+ This call generates and returns a binary WebMO archive (tar/zip) file from the specified job(s).
252
269
 
253
270
  Arguments:
254
- job_number(int): The job about whom to generate the archive
271
+ *job_numbers(int): The job(s) for which to generate the archive
255
272
 
256
273
  Returns:
257
274
  The raw data (as a string) of the WebMO archive, appropriate for saving to disk
258
275
  """
276
+
277
+ self._check_valid_job_list(job_numbers)
278
+
279
+ #append other relevant parameters
280
+ params = self._auth.copy()
281
+ params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
282
+ r = requests.get(self._base_url + "/jobs/archive" % (), params=params)
283
+ r.raise_for_status()
284
+ return r.text
285
+
286
+ def get_job_spreadsheet(self, *job_numbers):
287
+ """Returns a spreadsheet summary of the specified job(s).
288
+
289
+ This call generates and returns a CSV-formatted spreadsheet summary of the specified job(s).
259
290
 
260
- r = requests.get(self._base_url + "/jobs/%d/archive" % job_number, params=self._auth)
291
+ Arguments:
292
+ *job_numbers(int): The job(s) for which to generate the spreadsheet
293
+
294
+ Returns:
295
+ The raw data (as a string) of the spreadsheet, appropriate for saving to disk
296
+ """
297
+
298
+ self._check_valid_job_list(job_numbers)
299
+
300
+ #append other relevant parameters
301
+ params = self._auth.copy()
302
+ params.update({'jobNumber' : ",".join(str(job_number) for job_number in job_numbers)})
303
+ r = requests.get(self._base_url + "/jobs/spreadsheet" % (), params=params)
261
304
  r.raise_for_status()
262
305
  return r.text
263
306
 
@@ -273,7 +316,7 @@ class WebMOREST:
273
316
  r = requests.delete(self._base_url + "/jobs/%d" % job_number, params=self._auth)
274
317
  r.raise_for_status()
275
318
 
276
- def import_job(self, job_name, filename, engine):
319
+ def import_job(self, job_name, output_filename, engine):
277
320
  """Imports an existing output file into WebMO
278
321
 
279
322
  This call imports an existing computational output file, parsing the output and creating a newly
@@ -281,7 +324,7 @@ class WebMOREST:
281
324
 
282
325
  Arguments:
283
326
  job_name(str): The name of the new WebMO job
284
- filename(str): The filename (full path) of an existing output file to import
327
+ output_filename(str): The filename (full path) of an existing output file to import
285
328
  engine(str): The name of the computational engine
286
329
 
287
330
  Returns:
@@ -290,31 +333,111 @@ class WebMOREST:
290
333
 
291
334
  params = self._auth.copy()
292
335
  params.update({'jobName' : job_name, 'engine' : engine})
293
- output_file = {'outputFile' : ('output.log', open(filename, 'rb'), 'text/plain')}
336
+ output_file = {'outputFile' : ('output.log', open(output_filename, 'rb'), 'text/plain')}
294
337
  r = requests.post(self._base_url + '/jobs', data=params, files=output_file)
295
338
  r.raise_for_status()
296
339
  return r.json()["jobNumber"]
297
340
 
298
- def submit_job(self, job_name, input_file_contents, engine, queue=None):
341
+ def submit_job(self, job_name, input_contents, engine, queue=None, nodes=1, ppn=1, **kwargs):
299
342
  """Submits and executes a new WebMO job
300
343
 
301
- This call submits and executes a new job to a computational engine, generating a new WebMO job.
344
+ This call submits and executes a new job to a computational engine, generating a new WebMO job,
345
+ using nodes*ppn total cores.
302
346
 
303
347
  Arguments:
304
348
  job_name(str): The name of the new WebMO job
305
- input_file_contents(str): The contents of a valid input file to submit to a computational engine
349
+ input_contents(str): The valid input for submission to the computational engine as its input file
306
350
  engine(str): The name of the computational engine
351
+ queue(str, optional): The name of the queue in which to submit the job_name (required for external queues)
352
+ nodes(int, optional): The number of nodes to use; defaults to 1
353
+ ppn(int, optional): The number of cores per node to use; defaults to 1
354
+ **kwargs(optional): Additional optional fields such as nodeType [PBS],
355
+ resourceList (aka constraints) [SGE/SLURM], timeLimit [SLURM], gpus [SLURM]
307
356
 
308
357
  Returns:
309
- The the job number of the new job, upon success
358
+ The the job number of the new job, upon success.
310
359
  """
311
360
 
312
361
  params = self._auth.copy()
313
- params.update({'jobName' : job_name, 'engine' : engine, 'inputFile': input_file_contents, 'queue': queue})
362
+ params.update({'jobName' : job_name, 'engine' : engine, 'inputFile': input_contents,
363
+ 'queue': queue, 'nodes': nodes, 'ppn': ppn, **kwargs})
314
364
  r = requests.post(self._base_url + '/jobs', data=params)
315
365
  r.raise_for_status()
316
366
  return r.json()["jobNumber"]
317
367
 
368
+ #
369
+ # Template resource
370
+ #
371
+ def get_templates(self, engine):
372
+ """Fetches a list of job templates available to the current user
373
+
374
+ This call returns a dictionary of available templates and associated job variables.
375
+
376
+ Arguments:
377
+ engine(str): The name of the computational engine for which to fetch templates.
378
+
379
+ Returns:
380
+ A dictionary of available templates
381
+ """
382
+
383
+ r = requests.get(self._base_url + "/templates/%s" % engine, params=self._auth)
384
+ r.raise_for_status()
385
+ return r.json()["templates"]
386
+
387
+
388
+ def generate_input(self, template, variables, auto_defaults=True):
389
+ """Generates an input file from the specified template and dictionary of template job variables.
390
+
391
+ This call returns a text-formatted input file appropriate for submission.
392
+
393
+ Arguments:
394
+ template(dict): The desired job tempate (from get_templates)
395
+ variables(dict): A dictionary of variables to be used to generate the input file from the template.
396
+ auto_defaults(bool): Automatically use WebMO default values for omitted template variables
397
+
398
+ Returns:
399
+ A text-formatted input file
400
+ """
401
+ amend_variables = variables.copy()
402
+ if auto_defaults:
403
+ #update and amend 'variables' with default values for missing template parameters
404
+ for var in template['variables']:
405
+ if not var in variables:
406
+ amend_variables[var] = template['variables'][var]['default']
407
+
408
+ #append other relevant paramters
409
+ params = self._auth.copy()
410
+ params.update({'variables' : json.dumps(amend_variables)})
411
+ r = requests.get(self._base_url + "/templates/%s" % template['id'], params=params)
412
+ r.raise_for_status()
413
+ return r.text
414
+
415
+ #
416
+ # Engines resource
417
+ #
418
+ def get_engines(self, target_user=""):
419
+ """Fetches a list of computational engines available to the current user or specified target user
420
+
421
+ This call returns a JSON-formatted list of available engines and associated queues.
422
+
423
+ Arguments:
424
+ target_user(str, optional): The target username whose folders are retrieved. Otherwise, uses the authenticated user.
425
+
426
+ Returns:
427
+ A JSON-formatted list of available engines, associated queues/servers, and
428
+ node and processor limits.
429
+ """
430
+
431
+ #append other relevant parameters
432
+ params = self._auth.copy()
433
+ params.update({'user' : target_user})
434
+ r = requests.get(self._base_url + "/engines", params=params)
435
+ r.raise_for_status()
436
+ return r.json()["engines"]
437
+
438
+ #
439
+ # Jupyter-related
440
+ #
318
441
  async def display_job_property(self, job_number, property_name, property_index=1, peak_width=0.0, tms_shift=0.0, proton_coupling=0.0, nmr_field=400.0, x_range=None, y_range=None, width=400, height=400, background_color=(255,255,255), transparent_background=False, rotate=(0.,0.,0.)):
319
442
  """Uses Javascript and IPython to display an image of the specified molecule and property,
320
443
  calculated from a previous WebMO job.
@@ -683,7 +806,7 @@ class WebMOREST:
683
806
 
684
807
  async def _process_callback_response(self):
685
808
  from base64 import b64decode
686
- TIMEOUT = 10
809
+ TIMEOUT = 60
687
810
 
688
811
  json_msg = None
689
812
  for i in range(TIMEOUT):
@@ -1,12 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: webmo
3
- Version: 1.2.0
3
+ Version: 1.3.3
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.2.0.tar.gz
6
+ Download-URL: https://github.com/jrschmidt2/webmo-python/archive/refs/tags/1.3.3.tar.gz
7
7
  Author: J.R. Schmidt
8
8
  Author-email: schmidt@webmo.net
9
9
  License: MIT
10
10
  Keywords: WEBMO,REST,API
11
11
  Provides-Extra: spectrum
12
+ Provides-Extra: util
13
+ Provides-Extra: gui
14
+ Provides-Extra: all
12
15
  License-File: LICENSE
@@ -3,7 +3,9 @@ README.md
3
3
  setup.cfg
4
4
  setup.py
5
5
  webmo/__init__.py
6
+ webmo/gui.py
6
7
  webmo/spectrum.py
8
+ webmo/util.py
7
9
  webmo/webmo_rest.py
8
10
  webmo.egg-info/PKG-INFO
9
11
  webmo.egg-info/SOURCES.txt
@@ -0,0 +1,18 @@
1
+ requests
2
+ websockets
3
+
4
+ [all]
5
+ numpy
6
+ PubChemPy
7
+ questionary
8
+ ipywidgets
9
+
10
+ [gui]
11
+ questionary
12
+ ipywidgets
13
+
14
+ [spectrum]
15
+ numpy
16
+
17
+ [util]
18
+ PubChemPy
@@ -1,5 +0,0 @@
1
- requests
2
- websockets
3
-
4
- [spectrum]
5
- numpy
File without changes
File without changes
File without changes
File without changes