pma-python 2.0.0.185__tar.gz → 2.0.0.186__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.
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/PKG-INFO +1 -1
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python/core.py +64 -16
- pma_python-2.0.0.186/pma_python/version.py +1 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python.egg-info/PKG-INFO +1 -1
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python.egg-info/SOURCES.txt +2 -1
- pma_python-2.0.0.186/tests/test_slide_package.py +143 -0
- pma_python-2.0.0.185/pma_python/version.py +0 -1
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/README.md +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python/__init__.py +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python/control.py +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python/core_admin.py +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python/pma.py +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python/pma_core_client.py +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python/view.py +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python.egg-info/dependency_links.txt +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python.egg-info/not-zip-safe +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python.egg-info/requires.txt +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/pma_python.egg-info/top_level.txt +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/setup.cfg +0 -0
- {pma_python-2.0.0.185 → pma_python-2.0.0.186}/setup.py +0 -0
|
@@ -1935,13 +1935,68 @@ def _is_upload_type(upload_type, *candidates) -> bool:
|
|
|
1935
1935
|
return False
|
|
1936
1936
|
|
|
1937
1937
|
|
|
1938
|
+
def _companion_folders(slide_path: str) -> list[str]:
|
|
1939
|
+
"""
|
|
1940
|
+
Folders that hold the image data of a multi-file slide, by vendor naming convention:
|
|
1941
|
+
- "<name>/" e.g. 3DHistech MRXS
|
|
1942
|
+
- "_<name>_/" e.g. Olympus/Evident VSI
|
|
1943
|
+
"""
|
|
1944
|
+
directory = os.path.dirname(slide_path)
|
|
1945
|
+
stem = os.path.splitext(os.path.basename(slide_path))[0]
|
|
1946
|
+
return [os.path.join(directory, stem), os.path.join(directory, f"_{stem}_")]
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
def _pma_start_files_for_slide(slide_path: str) -> list[str]:
|
|
1950
|
+
"""
|
|
1951
|
+
Ask PMA.start which local files belong to this slide (it knows every multi-file format).
|
|
1952
|
+
Best effort: returns [] when PMA.start is not running or cannot answer.
|
|
1953
|
+
"""
|
|
1954
|
+
try:
|
|
1955
|
+
return list(get_files_for_slide(slide_path, _pma_pmacoreliteSessionID) or [])
|
|
1956
|
+
except Exception:
|
|
1957
|
+
if pma._pma_debug == True:
|
|
1958
|
+
print("PMA.start could not enumerate the files for", slide_path)
|
|
1959
|
+
return []
|
|
1960
|
+
|
|
1961
|
+
|
|
1938
1962
|
def _collect_slide_package_files(slide_path: str):
|
|
1963
|
+
"""
|
|
1964
|
+
All local files that make up a slide: the main file first, then its image data files.
|
|
1965
|
+
Files are found both by vendor folder conventions (see _companion_folders) and by asking
|
|
1966
|
+
PMA.start, so a multi-file slide is never uploaded without its image data.
|
|
1967
|
+
Relative paths are relative to the folder that contains the main file.
|
|
1968
|
+
"""
|
|
1939
1969
|
if not os.path.isfile(slide_path):
|
|
1940
1970
|
raise ValueError(f"File not found: {slide_path}")
|
|
1941
1971
|
|
|
1972
|
+
slide_path = os.path.abspath(slide_path)
|
|
1973
|
+
base_dir = os.path.dirname(slide_path)
|
|
1942
1974
|
main_name = os.path.basename(slide_path)
|
|
1943
|
-
|
|
1944
|
-
|
|
1975
|
+
|
|
1976
|
+
def key(path):
|
|
1977
|
+
return os.path.normcase(os.path.abspath(path))
|
|
1978
|
+
|
|
1979
|
+
extra = {}
|
|
1980
|
+
|
|
1981
|
+
def add(path):
|
|
1982
|
+
path = os.path.abspath(path)
|
|
1983
|
+
if key(path) == key(slide_path) or not os.path.isfile(path):
|
|
1984
|
+
return
|
|
1985
|
+
rel = os.path.relpath(path, base_dir)
|
|
1986
|
+
if rel.startswith(os.pardir) or os.path.isabs(rel):
|
|
1987
|
+
# never upload files from outside the slide's own folder
|
|
1988
|
+
return
|
|
1989
|
+
extra.setdefault(key(path), (path, rel.replace("\\", "/")))
|
|
1990
|
+
|
|
1991
|
+
for folder in _companion_folders(slide_path):
|
|
1992
|
+
if os.path.isdir(folder):
|
|
1993
|
+
for root, dirnames, filenames in os.walk(folder):
|
|
1994
|
+
dirnames.sort()
|
|
1995
|
+
for filename in sorted(filenames):
|
|
1996
|
+
add(os.path.join(root, filename))
|
|
1997
|
+
|
|
1998
|
+
for path in _pma_start_files_for_slide(slide_path):
|
|
1999
|
+
add(path)
|
|
1945
2000
|
|
|
1946
2001
|
package_files = [{
|
|
1947
2002
|
"local_path": slide_path,
|
|
@@ -1949,20 +2004,13 @@ def _collect_slide_package_files(slide_path: str):
|
|
|
1949
2004
|
"is_main": True,
|
|
1950
2005
|
"size": os.stat(slide_path).st_size
|
|
1951
2006
|
}]
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
rel_inside_stack = os.path.relpath(local_path, stack_root).replace("\\", "/")
|
|
1960
|
-
package_files.append({
|
|
1961
|
-
"local_path": local_path,
|
|
1962
|
-
"relative_path": f"{child_folder}/{rel_inside_stack}",
|
|
1963
|
-
"is_main": False,
|
|
1964
|
-
"size": os.stat(local_path).st_size
|
|
1965
|
-
})
|
|
2007
|
+
for path, rel in sorted(extra.values(), key=lambda item: item[1].lower()):
|
|
2008
|
+
package_files.append({
|
|
2009
|
+
"local_path": path,
|
|
2010
|
+
"relative_path": rel,
|
|
2011
|
+
"is_main": False,
|
|
2012
|
+
"size": os.stat(path).st_size
|
|
2013
|
+
})
|
|
1966
2014
|
|
|
1967
2015
|
return package_files
|
|
1968
2016
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.0.0.186'
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Tests for _collect_slide_package_files: multi-file slides must be uploaded with their image data."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
from pma_python import core
|
|
10
|
+
|
|
11
|
+
REAL_PMA_START_HELPER = core._pma_start_files_for_slide
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write(path, size):
|
|
15
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
16
|
+
with open(path, "wb") as f:
|
|
17
|
+
f.write(b"x" * size)
|
|
18
|
+
return path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CollectSlidePackageFiles(unittest.TestCase):
|
|
22
|
+
def setUp(self):
|
|
23
|
+
self.dir = tempfile.mkdtemp()
|
|
24
|
+
# by default PMA.start is not running
|
|
25
|
+
p = mock.patch.object(core, "_pma_start_files_for_slide", return_value=[])
|
|
26
|
+
p.start()
|
|
27
|
+
self.addCleanup(p.stop)
|
|
28
|
+
|
|
29
|
+
def tearDown(self):
|
|
30
|
+
shutil.rmtree(self.dir, ignore_errors=True)
|
|
31
|
+
|
|
32
|
+
def rel(self, files):
|
|
33
|
+
return [f["relative_path"] for f in files]
|
|
34
|
+
|
|
35
|
+
def test_olympus_vsi_underscore_folder(self):
|
|
36
|
+
vsi = write(os.path.join(self.dir, "VL1_Name, A_1AH&E.vsi"), 100)
|
|
37
|
+
write(os.path.join(self.dir, "_VL1_Name, A_1AH&E_", "stack1", "frame_t.ets"), 5000)
|
|
38
|
+
write(os.path.join(self.dir, "_VL1_Name, A_1AH&E_", "stack10001", "frame_t.ets"), 300)
|
|
39
|
+
files = core._collect_slide_package_files(vsi)
|
|
40
|
+
self.assertEqual(self.rel(files), [
|
|
41
|
+
"VL1_Name, A_1AH&E.vsi",
|
|
42
|
+
"_VL1_Name, A_1AH&E_/stack1/frame_t.ets",
|
|
43
|
+
"_VL1_Name, A_1AH&E_/stack10001/frame_t.ets",
|
|
44
|
+
])
|
|
45
|
+
self.assertTrue(files[0]["is_main"])
|
|
46
|
+
self.assertFalse(any(f["is_main"] for f in files[1:]))
|
|
47
|
+
self.assertEqual(core._get_slide_package_size(vsi), (5400, 5000))
|
|
48
|
+
|
|
49
|
+
def test_mrxs_same_name_folder(self):
|
|
50
|
+
mrxs = write(os.path.join(self.dir, "slide.mrxs"), 10)
|
|
51
|
+
write(os.path.join(self.dir, "slide", "Data0000.dat"), 700)
|
|
52
|
+
write(os.path.join(self.dir, "slide", "Slidedat.ini"), 20)
|
|
53
|
+
self.assertEqual(self.rel(core._collect_slide_package_files(mrxs)),
|
|
54
|
+
["slide.mrxs", "slide/Data0000.dat", "slide/Slidedat.ini"])
|
|
55
|
+
|
|
56
|
+
def test_single_file_slide(self):
|
|
57
|
+
svs = write(os.path.join(self.dir, "single.svs"), 42)
|
|
58
|
+
write(os.path.join(self.dir, "other.svs"), 42) # unrelated neighbour
|
|
59
|
+
files = core._collect_slide_package_files(svs)
|
|
60
|
+
self.assertEqual(self.rel(files), ["single.svs"])
|
|
61
|
+
self.assertEqual(core._get_slide_package_size(svs), (42, 42))
|
|
62
|
+
|
|
63
|
+
def test_pma_start_list_is_merged_without_duplicates(self):
|
|
64
|
+
vsi = write(os.path.join(self.dir, "a.vsi"), 1)
|
|
65
|
+
ets = write(os.path.join(self.dir, "_a_", "stack1", "frame_t.ets"), 9)
|
|
66
|
+
extra = write(os.path.join(self.dir, "a_extra", "x.bin"), 3) # a convention we do not know
|
|
67
|
+
outside = write(os.path.join(tempfile.mkdtemp(), "elsewhere.bin"), 3)
|
|
68
|
+
with mock.patch.object(core, "_pma_start_files_for_slide", return_value=[vsi, ets, extra, outside]):
|
|
69
|
+
files = core._collect_slide_package_files(vsi)
|
|
70
|
+
self.assertEqual(self.rel(files), ["a.vsi", "_a_/stack1/frame_t.ets", "a_extra/x.bin"])
|
|
71
|
+
shutil.rmtree(os.path.dirname(outside), ignore_errors=True)
|
|
72
|
+
|
|
73
|
+
def test_pma_start_unavailable_is_not_fatal(self):
|
|
74
|
+
vsi = write(os.path.join(self.dir, "b.vsi"), 1)
|
|
75
|
+
write(os.path.join(self.dir, "_b_", "stack1", "frame_t.ets"), 9)
|
|
76
|
+
with mock.patch.object(core, "get_files_for_slide", side_effect=ConnectionError("PMA.start down")), \
|
|
77
|
+
mock.patch.object(core, "_pma_start_files_for_slide", side_effect=REAL_PMA_START_HELPER):
|
|
78
|
+
files = core._collect_slide_package_files(vsi)
|
|
79
|
+
self.assertEqual(self.rel(files), ["b.vsi", "_b_/stack1/frame_t.ets"])
|
|
80
|
+
|
|
81
|
+
def test_missing_file(self):
|
|
82
|
+
with self.assertRaises(ValueError):
|
|
83
|
+
core._collect_slide_package_files(os.path.join(self.dir, "nope.vsi"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RealPmaStartHelper(unittest.TestCase):
|
|
87
|
+
def test_returns_empty_list_when_pma_start_errors(self):
|
|
88
|
+
with mock.patch.object(core, "get_files_for_slide", side_effect=ConnectionError("down")):
|
|
89
|
+
self.assertEqual(core._pma_start_files_for_slide("C:/x.vsi"), [])
|
|
90
|
+
|
|
91
|
+
def test_returns_paths_from_pma_start(self):
|
|
92
|
+
with mock.patch.object(core, "get_files_for_slide", return_value={"C:/x.vsi": {}, "C:/_x_/s/f.ets": {}}):
|
|
93
|
+
self.assertEqual(sorted(core._pma_start_files_for_slide("C:/x.vsi")), ["C:/_x_/s/f.ets", "C:/x.vsi"])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CloudUploadSendsWholeSlide(unittest.TestCase):
|
|
97
|
+
"""End to end through upload_file_on_cloud with a fake server: every file must be announced and sent."""
|
|
98
|
+
|
|
99
|
+
def test_vsi_upload_sends_image_data(self):
|
|
100
|
+
d = tempfile.mkdtemp()
|
|
101
|
+
self.addCleanup(shutil.rmtree, d, True)
|
|
102
|
+
vsi = write(os.path.join(d, "VL9_X, Y_1AH&E.vsi"), 100)
|
|
103
|
+
write(os.path.join(d, "_VL9_X, Y_1AH&E_", "stack1", "frame_t.ets"), 5000)
|
|
104
|
+
|
|
105
|
+
sent = []
|
|
106
|
+
|
|
107
|
+
class FakeResp:
|
|
108
|
+
id, upload_type, urls, multipart_files = 7, 2, ["u1", "u2"], []
|
|
109
|
+
|
|
110
|
+
class FakeClient:
|
|
111
|
+
server_url = "https://pma.example/"
|
|
112
|
+
|
|
113
|
+
def __init__(self, url):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
async def upload_header(self, header, session_id):
|
|
117
|
+
self.header = header
|
|
118
|
+
sent.append(("header", [(f.path, f.length, f.isMain) for f in header.files]))
|
|
119
|
+
return FakeResp()
|
|
120
|
+
|
|
121
|
+
async def upload_file(self, upload_id, upload_type, url, rel, stream, session_id, **kw):
|
|
122
|
+
sent.append(("file", rel, len(stream.read())))
|
|
123
|
+
|
|
124
|
+
class Ok:
|
|
125
|
+
status_code = 200
|
|
126
|
+
|
|
127
|
+
with mock.patch.object(core, "PmaCoreClient", FakeClient), \
|
|
128
|
+
mock.patch.object(core, "_pma_start_files_for_slide", return_value=[]), \
|
|
129
|
+
mock.patch.object(core.requests, "get", return_value=Ok()), \
|
|
130
|
+
mock.patch.object(core, "get_slide_info", return_value={}):
|
|
131
|
+
ok, err = asyncio.run(core.upload_file_on_cloud(
|
|
132
|
+
pma_core_url="https://pma.example/", session_id="S", slide_path=vsi,
|
|
133
|
+
upload_directory="Slides/Received"))
|
|
134
|
+
|
|
135
|
+
self.assertTrue(ok, err)
|
|
136
|
+
self.assertEqual(sent[0], ("header", [("VL9_X, Y_1AH&E.vsi", 100, True),
|
|
137
|
+
("_VL9_X, Y_1AH&E_/stack1/frame_t.ets", 5000, False)]))
|
|
138
|
+
self.assertEqual(sent[1:], [("file", "VL9_X, Y_1AH&E.vsi", 100),
|
|
139
|
+
("file", "_VL9_X, Y_1AH&E_/stack1/frame_t.ets", 5000)])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
unittest.main(verbosity=2)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.0.0.185'
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|