jupyter-analysis-tools 1.7.0__py3-none-any.whl
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.
- jupyter_analysis_tools/__init__.py +13 -0
- jupyter_analysis_tools/analysis.py +47 -0
- jupyter_analysis_tools/binning.py +443 -0
- jupyter_analysis_tools/datalocations.py +128 -0
- jupyter_analysis_tools/datastore.py +173 -0
- jupyter_analysis_tools/distrib.py +444 -0
- jupyter_analysis_tools/git.py +75 -0
- jupyter_analysis_tools/plotting.py +70 -0
- jupyter_analysis_tools/readdata.py +193 -0
- jupyter_analysis_tools/ssfz2json.py +57 -0
- jupyter_analysis_tools/ssfz_compare.py +54 -0
- jupyter_analysis_tools/utils.py +262 -0
- jupyter_analysis_tools/widgets.py +89 -0
- jupyter_analysis_tools-1.7.0.dist-info/METADATA +807 -0
- jupyter_analysis_tools-1.7.0.dist-info/RECORD +20 -0
- jupyter_analysis_tools-1.7.0.dist-info/WHEEL +5 -0
- jupyter_analysis_tools-1.7.0.dist-info/entry_points.txt +3 -0
- jupyter_analysis_tools-1.7.0.dist-info/licenses/AUTHORS.rst +6 -0
- jupyter_analysis_tools-1.7.0.dist-info/licenses/LICENSE +9 -0
- jupyter_analysis_tools-1.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# plotting.py
|
|
3
|
+
|
|
4
|
+
import matplotlib
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
|
|
7
|
+
from .readdata import readPDH
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
# increase the limit for the warning to pop up
|
|
11
|
+
matplotlib.rcParams["figure.max_open_warning"] = 50
|
|
12
|
+
except TypeError: # ignore the error with Sphinx
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def initFigure(fig, width=80, aspectRatio=4.0 / 3.0, quiet=False):
|
|
17
|
+
mmInch = 25.4
|
|
18
|
+
fig.set_size_inches(width / mmInch, width / aspectRatio / mmInch)
|
|
19
|
+
w, h = fig.get_size_inches()
|
|
20
|
+
if not quiet:
|
|
21
|
+
print("initFigure() with ({w:.1f}x{h:.1f}) mm".format(w=w * mmInch, h=h * mmInch))
|
|
22
|
+
return fig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def createFigure(width=80, aspectRatio=4.0 / 3.0, quiet=False, **kwargs):
|
|
26
|
+
"""output figure width in mm"""
|
|
27
|
+
fig = plt.figure(
|
|
28
|
+
# tight_layout=dict(pad=0.05),
|
|
29
|
+
**kwargs
|
|
30
|
+
)
|
|
31
|
+
initFigure(fig, width, aspectRatio, quiet)
|
|
32
|
+
return fig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def plotVertBar(ax, xpos, ymax, **kwargs):
|
|
36
|
+
ax.plot((xpos, xpos), (0, ymax), **kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def plotColor(idx):
|
|
40
|
+
pltcol = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
|
41
|
+
# print(pltcol)
|
|
42
|
+
pltcol = ["gray", "lightskyblue", "steelblue", "red", "salmon"]
|
|
43
|
+
return pltcol[idx]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def lineWidth():
|
|
47
|
+
return plt.rcParams["lines.linewidth"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def plotPDH(filename, label, **kwargs):
|
|
51
|
+
"""Plot a given .PDH file with the given label (shown in legend) using pandas and readPDH()."""
|
|
52
|
+
q_range = kwargs.pop("q_range", None)
|
|
53
|
+
print_filename = kwargs.pop("print_filename", True) # default value from readdata()
|
|
54
|
+
df, _ = readPDH(filename, q_range=q_range, print_filename=print_filename)
|
|
55
|
+
df["e"] = df["e"].clip(lower=0)
|
|
56
|
+
defaults = dict(
|
|
57
|
+
yerr="e",
|
|
58
|
+
logx=True,
|
|
59
|
+
logy=True,
|
|
60
|
+
label=label,
|
|
61
|
+
grid=True,
|
|
62
|
+
figsize=(10, 5),
|
|
63
|
+
xlabel=r"$q$ (nm$^{{-1}}$)",
|
|
64
|
+
ylabel="Intensity",
|
|
65
|
+
ecolor="lightgray",
|
|
66
|
+
)
|
|
67
|
+
for k, v in defaults.items():
|
|
68
|
+
if k not in kwargs:
|
|
69
|
+
kwargs[k] = v
|
|
70
|
+
df.plot("q", "I", **kwargs)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# readdata.py
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
import warnings
|
|
6
|
+
import xml.etree.ElementTree as et
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def readdata(fpath, q_range=None, read_csv_args=None, print_filename=True):
|
|
14
|
+
"""Read a datafile pandas Dataframe
|
|
15
|
+
extract a file_name
|
|
16
|
+
select q-range: q_min <= q <= q_max
|
|
17
|
+
"""
|
|
18
|
+
fpath = Path(fpath)
|
|
19
|
+
if print_filename:
|
|
20
|
+
print(f"Reading file '{str(fpath)}'")
|
|
21
|
+
if read_csv_args is None:
|
|
22
|
+
read_csv_args = dict()
|
|
23
|
+
if "sep" not in read_csv_args:
|
|
24
|
+
read_csv_args.update(sep=r"\s+")
|
|
25
|
+
if "names" not in read_csv_args:
|
|
26
|
+
read_csv_args.update(names=("q", "I", "e"))
|
|
27
|
+
if "index_col" not in read_csv_args:
|
|
28
|
+
read_csv_args.update(index_col=False)
|
|
29
|
+
# print("f_read_data, read_csv_args:", read_csv_args) # for debugging
|
|
30
|
+
|
|
31
|
+
file_ext = fpath.suffix
|
|
32
|
+
if file_ext.lower() == ".pdh": # for PDH files
|
|
33
|
+
nrows = pd.read_csv(
|
|
34
|
+
fpath,
|
|
35
|
+
skiprows=2,
|
|
36
|
+
nrows=1,
|
|
37
|
+
usecols=[
|
|
38
|
+
0,
|
|
39
|
+
],
|
|
40
|
+
sep=r"\s+",
|
|
41
|
+
header=None,
|
|
42
|
+
).values[0, 0]
|
|
43
|
+
read_csv_args.update(skiprows=5, nrows=nrows)
|
|
44
|
+
df = pd.read_csv(fpath, **read_csv_args)
|
|
45
|
+
|
|
46
|
+
# select q-range
|
|
47
|
+
if q_range is not None:
|
|
48
|
+
q_min, q_max = q_range
|
|
49
|
+
df = df[(df.q > q_min) & (df.q < q_max)]
|
|
50
|
+
|
|
51
|
+
filename = fpath.stem.split("[")[0]
|
|
52
|
+
return df, filename
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
readPDH = readdata
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def convertValue(val):
|
|
59
|
+
val = val.strip()
|
|
60
|
+
try:
|
|
61
|
+
return int(val)
|
|
62
|
+
except ValueError:
|
|
63
|
+
try:
|
|
64
|
+
return float(val)
|
|
65
|
+
except ValueError:
|
|
66
|
+
pass
|
|
67
|
+
return val
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def xmlPDHToDict(root):
|
|
71
|
+
result = {}
|
|
72
|
+
stack = [(root, result)]
|
|
73
|
+
while stack:
|
|
74
|
+
elem, parentCont = stack.pop()
|
|
75
|
+
elemCont = {}
|
|
76
|
+
idx = -1
|
|
77
|
+
key = elem.attrib.pop("key", None)
|
|
78
|
+
if ( # get a unique key, the key can occur in multiple groups in PDH
|
|
79
|
+
key is not None and elem.tag == "group" and elem.attrib.get("id", None) is not None
|
|
80
|
+
):
|
|
81
|
+
key = elem.attrib.pop("id")
|
|
82
|
+
if ( # skip empty elements with a key only early
|
|
83
|
+
not len(list(elem))
|
|
84
|
+
and not len(elem.attrib)
|
|
85
|
+
and not (elem.text and len(elem.text.strip()))
|
|
86
|
+
):
|
|
87
|
+
continue
|
|
88
|
+
if elem.tag == "list":
|
|
89
|
+
elemCont = []
|
|
90
|
+
else: # add attributes & values to dict
|
|
91
|
+
# Attach text, if any
|
|
92
|
+
if elem.text and len(elem.text.strip()):
|
|
93
|
+
if elem.tag in ("value", "reference"):
|
|
94
|
+
elemCont["value"] = convertValue(elem.text)
|
|
95
|
+
else:
|
|
96
|
+
elemCont["#text"] = convertValue(elem.text)
|
|
97
|
+
# Attach attributes, if any
|
|
98
|
+
if elem.attrib:
|
|
99
|
+
elemCont.update(
|
|
100
|
+
{k: convertValue(v) for k, v in elem.attrib.items() if len(v.strip())}
|
|
101
|
+
)
|
|
102
|
+
if key == "unit" and "value" in elemCont: # fix some units
|
|
103
|
+
elemCont["value"] = elemCont["value"].replace("_", "")
|
|
104
|
+
if "unit" in elemCont:
|
|
105
|
+
elemCont["unit"] = elemCont["unit"].replace("_", "")
|
|
106
|
+
# reduce the extracted dict&attributes
|
|
107
|
+
idx = elemCont.get("index", -1) # insert last/append if no index given
|
|
108
|
+
value = elemCont.get("value", None)
|
|
109
|
+
if value is not None and (
|
|
110
|
+
len(elemCont) == 1 or (len(elemCont) == 2 and "index" in elemCont)
|
|
111
|
+
):
|
|
112
|
+
elemCont = value # contains value only
|
|
113
|
+
parentKey = elem.tag
|
|
114
|
+
if key is not None and parentKey in ("list", "value", "group"):
|
|
115
|
+
# skip one level in hierarchy for these generic containers
|
|
116
|
+
parentKey = key
|
|
117
|
+
key = None
|
|
118
|
+
try:
|
|
119
|
+
if isinstance(parentCont, list):
|
|
120
|
+
parentCont.insert(idx, elemCont)
|
|
121
|
+
elif parentKey not in parentCont: # add as new list
|
|
122
|
+
if key is None: # make a list
|
|
123
|
+
parentCont[parentKey] = elemCont
|
|
124
|
+
else: # have a key
|
|
125
|
+
parentCont[parentKey] = {key: elemCont}
|
|
126
|
+
else: # parentKey exists already
|
|
127
|
+
if not isinstance(parentCont[parentKey], list) and not isinstance(
|
|
128
|
+
parentCont[parentKey], dict
|
|
129
|
+
):
|
|
130
|
+
# if its a plain value before, make a list out of it and append in next step
|
|
131
|
+
parentCont[parentKey] = [parentCont[parentKey]]
|
|
132
|
+
if isinstance(parentCont[parentKey], list):
|
|
133
|
+
parentCont[parentKey].append(elemCont)
|
|
134
|
+
elif key is not None:
|
|
135
|
+
parentCont[parentKey].update({key: elemCont})
|
|
136
|
+
else: # key is None
|
|
137
|
+
parentCont[parentKey].update(elemCont)
|
|
138
|
+
except AttributeError:
|
|
139
|
+
raise
|
|
140
|
+
# reversed for correct order
|
|
141
|
+
stack += [(child, elemCont) for child in reversed(list(elem))]
|
|
142
|
+
# fix some entry values, weird Anton Paar PDH format
|
|
143
|
+
try:
|
|
144
|
+
oldts = result["fileinfo"]["parameter"]["DateTime"]["value"]
|
|
145
|
+
# timestamp seems to be based on around 2009-01-01 (a day give or take)
|
|
146
|
+
delta = (39 * 365 + 10) * 24 * 3600
|
|
147
|
+
# make it compatible to datetime.datetime routines
|
|
148
|
+
result["fileinfo"]["parameter"]["DateTime"]["value"] = oldts + delta
|
|
149
|
+
except KeyError:
|
|
150
|
+
pass
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def readPDHmeta(pathPDH):
|
|
155
|
+
"""Reads the XML metadata at the end of a .PDH file to a Python dict."""
|
|
156
|
+
pathPDH = Path(pathPDH)
|
|
157
|
+
if pathPDH.suffix.lower() != ".pdh":
|
|
158
|
+
warnings.warn("readPDHmeta() supports .pdh files only!")
|
|
159
|
+
return # for PDH files
|
|
160
|
+
lines = ""
|
|
161
|
+
with open(pathPDH) as fd:
|
|
162
|
+
lines = fd.readlines()
|
|
163
|
+
nrows = int(lines[2].split()[0])
|
|
164
|
+
xml = "".join(lines[nrows + 5 :])
|
|
165
|
+
return xmlPDHToDict(et.fromstring(xml))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def readSSF(pathSSF):
|
|
169
|
+
"""Reads the SAXSquant session file *pathSSF* (.SSF) to a Python dict."""
|
|
170
|
+
pathSSF = Path(pathSSF)
|
|
171
|
+
if pathSSF.suffix.lower() != ".ssf":
|
|
172
|
+
warnings.warn("readSession() supports .ssf files only!")
|
|
173
|
+
return # for PDH files
|
|
174
|
+
data = ""
|
|
175
|
+
with open(pathSSF, encoding="utf-8-sig") as fd:
|
|
176
|
+
data = fd.read()
|
|
177
|
+
return xmlPDHToDict(et.fromstring(data))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def readSSFZ(pathSSFZ):
|
|
181
|
+
"""Extracts and reads the SAXSquant session file (.SSF) to a Python dict.
|
|
182
|
+
The .SSF is embedded in the .SSFZ provided by *pathSSFZ*."""
|
|
183
|
+
assert pathSSFZ.is_file()
|
|
184
|
+
# unpack the SSFZ to a temporary dir
|
|
185
|
+
data = None
|
|
186
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
187
|
+
with zipfile.ZipFile(pathSSFZ, "r") as zipfd:
|
|
188
|
+
zipfd.extractall(tempdir)
|
|
189
|
+
# read the session metadata from the extracted SSF file
|
|
190
|
+
pathSSF = next(Path(tempdir).glob("*.ssf"))
|
|
191
|
+
assert pathSSF.is_file()
|
|
192
|
+
data = readSSF(pathSSF)
|
|
193
|
+
return data
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ssfz2json.py
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from jupyter_analysis_tools.readdata import readSSFZ
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description="""
|
|
15
|
+
Reads and parses the embedded metadata of a .SSFZ file created by Anton Paar SAXSquant
|
|
16
|
+
software, converts it to JSON format and outputs it to <stdout>.
|
|
17
|
+
An output file path for the JSON data can be provided by optional argument.
|
|
18
|
+
"""
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"ssfzPath",
|
|
22
|
+
type=lambda p: Path(p).absolute(),
|
|
23
|
+
help="Path of the input .SSFZ file to read.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-o",
|
|
27
|
+
"--out",
|
|
28
|
+
nargs="?",
|
|
29
|
+
default="stdout",
|
|
30
|
+
help=(
|
|
31
|
+
"Output file path to write the JSON data to. If the filename is omitted, "
|
|
32
|
+
"it is derived from the input file name by adding the .json suffix."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
args = parser.parse_args()
|
|
36
|
+
# print(args)
|
|
37
|
+
if not args.ssfzPath.is_file():
|
|
38
|
+
print(f"Provided file '{args.ssfzPath}' not found!")
|
|
39
|
+
return 1
|
|
40
|
+
data = readSSFZ(args.ssfzPath)
|
|
41
|
+
json_args = dict(sort_keys=True, indent=2)
|
|
42
|
+
if args.out == "stdout":
|
|
43
|
+
print(json.dumps(data, **json_args))
|
|
44
|
+
else:
|
|
45
|
+
if args.out is None:
|
|
46
|
+
args.out = args.ssfzPath.with_suffix(args.ssfzPath.suffix + ".json")
|
|
47
|
+
if not Path(args.out).parent.is_dir():
|
|
48
|
+
print(f"Directory of provided output file '{args.out}' does not exist!")
|
|
49
|
+
return 1
|
|
50
|
+
with open(args.out, "w") as fd:
|
|
51
|
+
json.dump(data, fd, **json_args)
|
|
52
|
+
print(f"Wrote '{args.out}'.")
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
sys.exit(main())
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ssfz2json.py
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import difflib
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from jupyter_analysis_tools.readdata import readSSFZ
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="""
|
|
16
|
+
Reads and parses the embedded metadata of two .SSFZ files created by Anton Paar
|
|
17
|
+
SAXSquant software, converts them to JSON format and performs a diff-like comparison
|
|
18
|
+
which is output on <stdout>.
|
|
19
|
+
"""
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"fromfile",
|
|
23
|
+
type=lambda p: Path(p).absolute(),
|
|
24
|
+
help="Path of the first .SSFZ file to compare.",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"tofile",
|
|
28
|
+
type=lambda p: Path(p).absolute(),
|
|
29
|
+
help="Path of the second .SSFZ file to compare to.",
|
|
30
|
+
)
|
|
31
|
+
json_args = dict(sort_keys=True, indent=2)
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
# print(args)
|
|
34
|
+
if not args.fromfile.is_file():
|
|
35
|
+
print(f"Provided file '{args.fromfile}' not found!")
|
|
36
|
+
return 1
|
|
37
|
+
if not args.tofile.is_file():
|
|
38
|
+
print(f"Provided file '{args.tofile}' not found!")
|
|
39
|
+
return 1
|
|
40
|
+
olddata = readSSFZ(args.fromfile)
|
|
41
|
+
newdata = readSSFZ(args.tofile)
|
|
42
|
+
diff = difflib.unified_diff(
|
|
43
|
+
json.dumps(olddata, **json_args).splitlines(keepends=True),
|
|
44
|
+
json.dumps(newdata, **json_args).splitlines(keepends=True),
|
|
45
|
+
fromfile=str(args.fromfile),
|
|
46
|
+
tofile=str(args.tofile),
|
|
47
|
+
)
|
|
48
|
+
for line in diff:
|
|
49
|
+
print(line, end="")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
sys.exit(main())
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# utils.py
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
5
|
+
import copy
|
|
6
|
+
import itertools
|
|
7
|
+
import locale
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
indent = " "
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setLocaleUTF8():
|
|
21
|
+
"""Fix the Jupyter locale which is not UTF-8 by default on Windows."""
|
|
22
|
+
locOld = locale.getpreferredencoding(False).lower()
|
|
23
|
+
|
|
24
|
+
def getpreferredencoding(do_setlocale=True):
|
|
25
|
+
return "utf-8"
|
|
26
|
+
|
|
27
|
+
locale.getpreferredencoding = getpreferredencoding
|
|
28
|
+
locNew = locale.getpreferredencoding(False)
|
|
29
|
+
if locOld != locNew:
|
|
30
|
+
print(f"Updated locale from {locOld} -> {locNew}.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def isLinux():
|
|
34
|
+
return platform.system().lower() in "linux"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def isMac():
|
|
38
|
+
return platform.system().lower() in "darwin"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def isWindows():
|
|
42
|
+
return platform.system().lower() in "windows"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def isList(obj):
|
|
46
|
+
"""Return true if the provided object is list-like including a numpy array but not a string.
|
|
47
|
+
|
|
48
|
+
>>> isList([1, 2, 'a'])
|
|
49
|
+
True
|
|
50
|
+
>>> isList(tuple((1, 2, 'a')))
|
|
51
|
+
True
|
|
52
|
+
>>> import numpy
|
|
53
|
+
>>> isList(numpy.arange(5))
|
|
54
|
+
True
|
|
55
|
+
>>> isList("dummy")
|
|
56
|
+
False
|
|
57
|
+
>>> isList(None)
|
|
58
|
+
False
|
|
59
|
+
"""
|
|
60
|
+
return isinstance(obj, (list, tuple, np.ndarray))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def shortenWinPath(path):
|
|
64
|
+
if not isWindows():
|
|
65
|
+
return path
|
|
66
|
+
import win32api
|
|
67
|
+
|
|
68
|
+
return win32api.GetShortPathName(path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def appendToPATH(parentPath, subdirs=None, verbose=False):
|
|
72
|
+
"""Adds the given path with each subdirectory to the PATH environment variable."""
|
|
73
|
+
parentPath = Path(parentPath)
|
|
74
|
+
if not parentPath.is_dir():
|
|
75
|
+
return # nothing to do
|
|
76
|
+
if subdirs is None:
|
|
77
|
+
subdirs = ["."]
|
|
78
|
+
sep = ";" if isWindows() else ":"
|
|
79
|
+
PATH = os.environ["PATH"].split(sep)
|
|
80
|
+
for path in subdirs:
|
|
81
|
+
path = parentPath / path
|
|
82
|
+
if verbose:
|
|
83
|
+
print(indent, path, "[exists: {}]".format(path.is_dir()))
|
|
84
|
+
if path not in PATH:
|
|
85
|
+
PATH.append(str(path))
|
|
86
|
+
os.environ["PATH"] = sep.join(PATH)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def addEnvScriptsToPATH():
|
|
90
|
+
"""Prepends the *Scripts* directory of the current Python environment base directory to systems
|
|
91
|
+
PATH variable.
|
|
92
|
+
|
|
93
|
+
It is intended for Conda (Miniforge) environments on Windows that do not have this in their PATH
|
|
94
|
+
environment variable, causing them to miss many commands provided from this location.
|
|
95
|
+
"""
|
|
96
|
+
envPath = [p for p in sys.path if p.endswith("Lib")]
|
|
97
|
+
if not envPath:
|
|
98
|
+
return # probably not a Miniforge environment
|
|
99
|
+
envPath = envPath[0]
|
|
100
|
+
envPath = Path(envPath).parent / "Scripts"
|
|
101
|
+
sep = ";" if isWindows() else ":"
|
|
102
|
+
environPATH = os.environ["PATH"].split(sep)
|
|
103
|
+
# print(environPATH)
|
|
104
|
+
if envPath.exists() and str(envPath) not in environPATH:
|
|
105
|
+
environPATH = [str(envPath)] + environPATH
|
|
106
|
+
os.environ["PATH"] = sep.join(environPATH)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def networkdriveMapping(cmdOutput: str = None, resolveNames: bool = True):
|
|
110
|
+
"""Returns a dict of mapping drive letters to network paths (on Windows)."""
|
|
111
|
+
if isWindows():
|
|
112
|
+
if cmdOutput is None:
|
|
113
|
+
proc = subprocess.run(["net", "use"], capture_output=True, text=True, encoding="cp850")
|
|
114
|
+
cmdOutput = proc.stdout
|
|
115
|
+
|
|
116
|
+
def resolveFQDN(uncPath):
|
|
117
|
+
if not resolveNames:
|
|
118
|
+
return uncPath
|
|
119
|
+
parts = uncPath.split("\\")
|
|
120
|
+
idx = [i for i, part in enumerate(parts) if len(part)][0]
|
|
121
|
+
proc = subprocess.run(
|
|
122
|
+
["nslookup", parts[idx]], capture_output=True, text=True, encoding="cp850"
|
|
123
|
+
)
|
|
124
|
+
res = [line.split() for line in proc.stdout.splitlines() if line.startswith("Name:")]
|
|
125
|
+
if len(res) and len(res[0]) == 2:
|
|
126
|
+
parts[idx] = res[0][1]
|
|
127
|
+
return "\\".join(parts)
|
|
128
|
+
|
|
129
|
+
rows = [line.split() for line in cmdOutput.splitlines() if "Windows Network" in line]
|
|
130
|
+
rows = {
|
|
131
|
+
row[1]: resolveFQDN(row[2])
|
|
132
|
+
for row in rows
|
|
133
|
+
if row[1].endswith(":") and row[2].startswith(r"\\")
|
|
134
|
+
}
|
|
135
|
+
return rows
|
|
136
|
+
else: # Linux (tested) or macOS (untested)
|
|
137
|
+
if cmdOutput is None:
|
|
138
|
+
proc = subprocess.run(["mount"], capture_output=True, text=True)
|
|
139
|
+
cmdOutput = proc.stdout
|
|
140
|
+
|
|
141
|
+
def parse(line):
|
|
142
|
+
# position of last opening parenthesis, start of options list
|
|
143
|
+
lastParen = list(i for i, c in enumerate(line) if "(" == c)[-1]
|
|
144
|
+
line = line[:lastParen].strip()
|
|
145
|
+
spaces = list(i for i, c in enumerate(line) if " " == c)
|
|
146
|
+
fstype = line[spaces[-1] :].strip() # last remaining word is the filesystem type
|
|
147
|
+
line = line[: spaces[-2]].strip() # strip the 'type' indicator as well
|
|
148
|
+
sepIdx = line.find(" on /") # separates destination from mount point
|
|
149
|
+
dest = line[:sepIdx].strip()
|
|
150
|
+
mountpoint = line[sepIdx + 4 :].strip()
|
|
151
|
+
yield (mountpoint, dest, fstype)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
mp: dst
|
|
155
|
+
for line in cmdOutput.strip().splitlines()
|
|
156
|
+
for (mp, dst, fstype) in parse(line)
|
|
157
|
+
if fstype in ("nfs", "cifs", "sshfs", "afs", "ext4")
|
|
158
|
+
}
|
|
159
|
+
return {}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def makeNetworkdriveAbsolute(filepath, cmdOutput: str = None, resolveNames: bool = True):
|
|
163
|
+
"""Replaces the drive letter of the given path by the respective network path, if possible."""
|
|
164
|
+
if filepath.drive.startswith(r"\\"):
|
|
165
|
+
return filepath # it's a UNC path already
|
|
166
|
+
if isWindows():
|
|
167
|
+
drivemap = networkdriveMapping(cmdOutput=cmdOutput, resolveNames=resolveNames)
|
|
168
|
+
prefix = drivemap.get(filepath.drive, None)
|
|
169
|
+
if prefix is not None:
|
|
170
|
+
filepath = Path(prefix).joinpath(*filepath.parts[1:])
|
|
171
|
+
else: # Linux or macOS
|
|
172
|
+
drivemap = networkdriveMapping(cmdOutput=cmdOutput, resolveNames=resolveNames)
|
|
173
|
+
# search for the mountpoint, starting with the longest, most specific, first
|
|
174
|
+
for mp, target in sorted(drivemap.items(), key=lambda tup: len(tup[0]), reverse=True):
|
|
175
|
+
if filepath.is_relative_to(mp):
|
|
176
|
+
return Path(target).joinpath(filepath.relative_to(mp))
|
|
177
|
+
return filepath
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def checkWinFor7z():
|
|
181
|
+
"""Extend the PATH environment variable for access to the 7-zip executable."""
|
|
182
|
+
if not isWindows():
|
|
183
|
+
return # tests below are intended for Windows
|
|
184
|
+
sevenzippath = r"C:\Program Files\7-Zip"
|
|
185
|
+
if not os.path.isdir(sevenzippath):
|
|
186
|
+
print(
|
|
187
|
+
"7-Zip not found in '{}'.\n".format(sevenzippath)
|
|
188
|
+
+ "7-Zip is required for managing data files and results!."
|
|
189
|
+
)
|
|
190
|
+
return
|
|
191
|
+
print("Adding the following directory to $PATH:")
|
|
192
|
+
appendToPATH(sevenzippath)
|
|
193
|
+
print("\nUpdated PATH:")
|
|
194
|
+
for path in os.environ["PATH"].split(";"):
|
|
195
|
+
print(indent, path)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def extract7z(fn, workdir=None):
|
|
199
|
+
assert os.path.isfile(os.path.join(workdir, fn)), "Provided 7z archive '{}' not found!".format(
|
|
200
|
+
fn
|
|
201
|
+
)
|
|
202
|
+
print(f"Extracting '{fn}': ")
|
|
203
|
+
proc = subprocess.run(
|
|
204
|
+
["7z", "x", fn],
|
|
205
|
+
cwd=workdir,
|
|
206
|
+
stdout=subprocess.PIPE,
|
|
207
|
+
stderr=subprocess.PIPE,
|
|
208
|
+
)
|
|
209
|
+
print(proc.stdout.decode(errors="ignore"))
|
|
210
|
+
if len(proc.stderr):
|
|
211
|
+
print("## stderr:\n", proc.stderr.decode(errors="ignore"))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# https://stackoverflow.com/a/13847807
|
|
215
|
+
@contextlib.contextmanager
|
|
216
|
+
def pushd(new_dir):
|
|
217
|
+
previous_dir = os.getcwd()
|
|
218
|
+
os.chdir(new_dir)
|
|
219
|
+
yield
|
|
220
|
+
os.chdir(previous_dir)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def setPackage(globalsdict):
|
|
224
|
+
"""Sets the current directory of the notebook as python package to make relative module imports
|
|
225
|
+
work.
|
|
226
|
+
|
|
227
|
+
Usage: `setPackage(globals())`
|
|
228
|
+
"""
|
|
229
|
+
path = Path().resolve()
|
|
230
|
+
searchpath = str(path.parent)
|
|
231
|
+
if searchpath not in sys.path:
|
|
232
|
+
sys.path.insert(0, searchpath)
|
|
233
|
+
globalsdict["__package__"] = path.name
|
|
234
|
+
globalsdict["__name__"] = path.name
|
|
235
|
+
print(f"Setting the current directory as package '{path.name}': \n {path}.")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def grouper(iterable, n, fillvalue=None):
|
|
239
|
+
"""Returns an iterator over a list of tuples (grouping) for a given flat iterable."""
|
|
240
|
+
args = [iter(iterable)] * n
|
|
241
|
+
return itertools.zip_longest(*args, fillvalue=fillvalue)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def fmtErr(val, std, precision=2, width=None):
|
|
245
|
+
"""Formats a given value and its stdandard deviation to physics notation, e.g. '1.23(4)'."""
|
|
246
|
+
if width is None:
|
|
247
|
+
width = ""
|
|
248
|
+
fmt = "{:" + str(width) + "." + str(precision) + "f}({:.0f})"
|
|
249
|
+
# print("fmtErr val:", val, "std:", std)
|
|
250
|
+
return fmt.format(val, std * 10 ** (precision))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def updatedDict(d, key, value):
|
|
254
|
+
"""Implements the \\|= operator for dict in Python version <3.9."""
|
|
255
|
+
dd = copy.copy(d)
|
|
256
|
+
dd[key] = value
|
|
257
|
+
return dd
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def naturalKey(name):
|
|
261
|
+
"""Split string into list of strings and integers. Use as *key* function for sorting files."""
|
|
262
|
+
return [int(text) if text.isdigit() else text.lower() for text in re.split(r"(\d+)", name)]
|