dsh-origin-bridge 0.4.1 → 0.4.3
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.
- package/README.md +299 -234
- package/asciiio.py +82 -12
- package/docs/preview.png +0 -0
- package/edge_checks.py +10 -0
- package/originlab.py +475 -50
- package/package.json +76 -74
- package/scripts/make_preview.py +77 -0
- package/server.py +612 -577
- package/smoke_test.py +37 -7
- package/test_lifecycle.py +232 -0
package/asciiio.py
CHANGED
|
@@ -25,15 +25,59 @@ def _is_number(token):
|
|
|
25
25
|
return bool(t) and bool(_NUM.match(t))
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
_NAME_KEYWORDS = ("long name", "longname", "name", "display", "l")
|
|
29
|
+
_UNIT_KEYWORDS = ("units", "unit", "u")
|
|
30
|
+
# "Potential/V", "电流 (mA)", "Z'/ohm": instruments pack the unit into the name.
|
|
31
|
+
_NAME_UNIT = re.compile(r"^(?P<name>[^/\(\[]+?)\s*(?:/\s*|\(|\[)\s*(?P<units>[^)\]]+?)\s*[\)\]]?$")
|
|
32
|
+
_PAIR = re.compile(r"^\s*(?P<key>[^=:]{2,40}?)\s*[=:]\s*(?P<value>.+?)\s*$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _row_cells(row, width):
|
|
36
|
+
return [str(row[c] if c < len(row) else "").strip() for c in range(width)]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _all_names(row, width):
|
|
40
|
+
cells = _row_cells(row, width)
|
|
41
|
+
return bool(cells) and all(cells) and not any(_is_number(c) for c in cells)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _units_like(row, width):
|
|
45
|
+
"""A units row is short text ("s", "mV", "-") with no name-like slashes."""
|
|
46
|
+
cells = [c for c in _row_cells(row, width) if c]
|
|
47
|
+
return bool(cells) and not any(_is_number(c) for c in cells) \
|
|
48
|
+
and all(len(c) <= 6 and "/" not in c and "(" not in c for c in cells)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def split_name_units(text):
|
|
52
|
+
m = _NAME_UNIT.match(str(text).strip())
|
|
53
|
+
if not m:
|
|
54
|
+
return str(text).strip(), ""
|
|
55
|
+
return m.group("name").strip(), m.group("units").strip()
|
|
56
|
+
|
|
57
|
+
|
|
28
58
|
def _pick_name_row(head, width):
|
|
29
|
-
"""
|
|
59
|
+
"""Which header row carries the column long names?
|
|
30
60
|
|
|
31
|
-
|
|
32
|
-
|
|
61
|
+
Origin's own exports use a labelled block (Long Name / Units / Comments), so
|
|
62
|
+
that row wins. Instrument dumps (CH Instruments .txt and friends) put a page
|
|
63
|
+
of settings first and the column line immediately above the data, so the last
|
|
64
|
+
all-text row is the name row. Taking head[0] blindly named every column
|
|
65
|
+
"Jan. 31" / 2011 15:12:13 on those files.
|
|
33
66
|
"""
|
|
67
|
+
for i, row in enumerate(head):
|
|
68
|
+
cells = _row_cells(row, width)
|
|
69
|
+
if cells and cells[0].lower() in _NAME_KEYWORDS and _all_names(row, width):
|
|
70
|
+
return i
|
|
71
|
+
if head and _all_names(head[-1], width):
|
|
72
|
+
# "Time Signal Flag" followed by "s mV -": the short row is the units row.
|
|
73
|
+
if len(head) >= 2 and _all_names(head[-2], width) and _units_like(head[-1], width):
|
|
74
|
+
return len(head) - 2
|
|
75
|
+
return len(head) - 1
|
|
76
|
+
if len(head) >= 2 and _all_names(head[-2], width) and _units_like(head[-1], width):
|
|
77
|
+
return len(head) - 2
|
|
34
78
|
best_i, best_score = 0, None
|
|
35
79
|
for i, row in enumerate(head):
|
|
36
|
-
toks =
|
|
80
|
+
toks = _row_cells(row, width)
|
|
37
81
|
uniq = len({t for t in toks if t})
|
|
38
82
|
score = uniq * 10 + sum(1 for t in toks if t)
|
|
39
83
|
if uniq <= 1 and width > 1:
|
|
@@ -43,6 +87,19 @@ def _pick_name_row(head, width):
|
|
|
43
87
|
return best_i
|
|
44
88
|
|
|
45
89
|
|
|
90
|
+
def read_metadata(head, k):
|
|
91
|
+
"""Key = Value / Key: Value lines above the column row, kept for provenance."""
|
|
92
|
+
meta = {}
|
|
93
|
+
for row in head[:k]:
|
|
94
|
+
line = ", ".join(c for c in row if c).strip()
|
|
95
|
+
m = _PAIR.match(line)
|
|
96
|
+
if m and not _is_number(m.group("key")):
|
|
97
|
+
meta.setdefault(m.group("key").strip(), m.group("value"))
|
|
98
|
+
if len(meta) >= 40:
|
|
99
|
+
break
|
|
100
|
+
return meta
|
|
101
|
+
|
|
102
|
+
|
|
46
103
|
def read_text(path):
|
|
47
104
|
"""Decode a data file, trying the encodings that actually occur in labs."""
|
|
48
105
|
last = None
|
|
@@ -85,6 +142,10 @@ def sniff_delimiter(rows):
|
|
|
85
142
|
width = max(len(g) for g in grid)
|
|
86
143
|
if width >= 2:
|
|
87
144
|
return None
|
|
145
|
+
if width == 1:
|
|
146
|
+
# "识别不出分隔符" would send the reader chasing encoding or comment
|
|
147
|
+
# prefixes; the real reason is that a single column cannot make a plot.
|
|
148
|
+
raise ValueError("文件只有一列数据:至少需要两列(x 和 y)才能解析成数据表")
|
|
88
149
|
raise ValueError("识别不出分隔符,也没有多列空白对齐的数据")
|
|
89
150
|
|
|
90
151
|
|
|
@@ -122,16 +183,24 @@ def parse(path, comment=None):
|
|
|
122
183
|
raise ValueError("找不到数值数据行:%s(只有表头?)" % path)
|
|
123
184
|
|
|
124
185
|
k = _pick_name_row(head, width) if head else 0
|
|
125
|
-
|
|
186
|
+
raw_names = _row_cells(head[k], width) if head else [""] * width
|
|
126
187
|
if k + 1 < len(head):
|
|
127
|
-
units =
|
|
128
|
-
elif k > 0:
|
|
129
|
-
units =
|
|
188
|
+
units = _row_cells(head[k + 1], width)
|
|
189
|
+
elif k > 0 and head[k - 1] and head[k - 1][0].strip().lower() in _UNIT_KEYWORDS:
|
|
190
|
+
units = _row_cells(head[k - 1], width)
|
|
130
191
|
else:
|
|
131
192
|
units = [""] * width
|
|
132
|
-
|
|
193
|
+
names = []
|
|
194
|
+
for c in range(width):
|
|
195
|
+
nm, un = split_name_units(raw_names[c])
|
|
196
|
+
names.append(nm)
|
|
197
|
+
if un and not units[c]:
|
|
198
|
+
units[c] = un
|
|
199
|
+
raw_names = names
|
|
200
|
+
metadata = read_metadata(head, k)
|
|
201
|
+
extra = k if metadata else max(0, len(head) - 2)
|
|
133
202
|
if extra:
|
|
134
|
-
warnings.append("忽略了 %d
|
|
203
|
+
warnings.append("忽略了 %d 行文件说明(仪器参数已放进 metadata)" % extra)
|
|
135
204
|
|
|
136
205
|
columns = []
|
|
137
206
|
for c in range(width):
|
|
@@ -147,11 +216,11 @@ def parse(path, comment=None):
|
|
|
147
216
|
vals.append(None)
|
|
148
217
|
live = sum(1 for v in vals if v is not None)
|
|
149
218
|
if live == 0:
|
|
150
|
-
warnings.append("第 %d 列 %r 解析后全为空,已跳过" % (c + 1,
|
|
219
|
+
warnings.append("第 %d 列 %r 解析后全为空,已跳过" % (c + 1, raw_names[c] or "col%d" % (c + 1)))
|
|
151
220
|
continue
|
|
152
221
|
columns.append({
|
|
153
222
|
"index": c + 1,
|
|
154
|
-
"name":
|
|
223
|
+
"name": raw_names[c] or "col%d" % (c + 1),
|
|
155
224
|
"units": units[c] or "",
|
|
156
225
|
"values": vals,
|
|
157
226
|
"numeric_rows": live,
|
|
@@ -165,5 +234,6 @@ def parse(path, comment=None):
|
|
|
165
234
|
"rows": len(body),
|
|
166
235
|
"header_rows": len(head),
|
|
167
236
|
"columns": columns,
|
|
237
|
+
"metadata": metadata,
|
|
168
238
|
"warnings": warnings,
|
|
169
239
|
}
|
package/docs/preview.png
CHANGED
|
Binary file
|
package/edge_checks.py
CHANGED
|
@@ -173,6 +173,16 @@ def main():
|
|
|
173
173
|
print("SKIP node launcher (node not on PATH)")
|
|
174
174
|
|
|
175
175
|
bad = [n for n, ok in results if not ok]
|
|
176
|
+
# Same teardown as smoke_test: reclaim only the windowless instances this run
|
|
177
|
+
# started; an older one may belong to a live dsh session.
|
|
178
|
+
late = [r for r in S.originlab._process_rows()
|
|
179
|
+
if not r["window"] and str(r.get("started") or "") >= S.STARTED_AT]
|
|
180
|
+
for row in late:
|
|
181
|
+
S.originlab._close_pid(row["pid"])
|
|
182
|
+
left = [r for r in S.originlab._process_rows()
|
|
183
|
+
if not r["window"] and str(r.get("started") or "") >= S.STARTED_AT]
|
|
184
|
+
check("run leaves no Origin instance", not left, left)
|
|
185
|
+
|
|
176
186
|
print("\n%d/%d passed" % (len(results) - len(bad), len(results)))
|
|
177
187
|
if bad:
|
|
178
188
|
print("FAILED:", ", ".join(bad))
|