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.
- {webmo-1.4.2/webmo.egg-info → webmo-1.5.1}/PKG-INFO +2 -2
- {webmo-1.4.2 → webmo-1.5.1}/webmo/__init__.py +1 -1
- {webmo-1.4.2 → webmo-1.5.1}/webmo/util.py +83 -12
- {webmo-1.4.2 → webmo-1.5.1}/webmo/webmo_rest.py +37 -21
- {webmo-1.4.2 → webmo-1.5.1/webmo.egg-info}/PKG-INFO +2 -2
- {webmo-1.4.2 → webmo-1.5.1}/LICENSE +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/README.md +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/setup.cfg +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/setup.py +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/webmo/gui.py +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/webmo/spectrum.py +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/webmo.egg-info/SOURCES.txt +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/webmo.egg-info/dependency_links.txt +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/webmo.egg-info/requires.txt +0 -0
- {webmo-1.4.2 → webmo-1.5.1}/webmo.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: webmo
|
|
3
|
-
Version: 1.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|