pma-python 2.0.0.179__tar.gz → 2.0.0.181__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.
Files changed (20) hide show
  1. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/PKG-INFO +1 -1
  2. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/core.py +284 -130
  3. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/pma_core_client.py +38 -23
  4. pma_python-2.0.0.181/pma_python/version.py +1 -0
  5. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/PKG-INFO +1 -1
  6. pma_python-2.0.0.179/pma_python/version.py +0 -1
  7. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/README.md +0 -0
  8. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/long_desc.txt +0 -0
  9. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/__init__.py +0 -0
  10. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/control.py +0 -0
  11. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/core_admin.py +0 -0
  12. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/pma.py +0 -0
  13. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/view.py +0 -0
  14. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/SOURCES.txt +0 -0
  15. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/dependency_links.txt +0 -0
  16. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/not-zip-safe +0 -0
  17. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/requires.txt +0 -0
  18. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/top_level.txt +0 -0
  19. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/setup.cfg +0 -0
  20. {pma_python-2.0.0.179 → pma_python-2.0.0.181}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pma_python
3
- Version: 2.0.0.179
3
+ Version: 2.0.0.181
4
4
  Summary: Universal viewing of digital microscopy, whole slide imaging and digital pathology data
5
5
  Home-page: https://github.com/pathomation/pma_python
6
6
  Author: Pathomation
@@ -18,7 +18,7 @@ import pandas as pd
18
18
  import requests
19
19
  from requests_toolbelt.multipart.encoder import MultipartEncoder, MultipartEncoderMonitor
20
20
 
21
- from .pma_core_client import PmaCoreClient, UploadHeaderModel, UploadFileModel
21
+ from .pma_core_client import PmaCoreClient, UploadHeaderModel, UploadFileModel, UploadResponse
22
22
  from typing import Callable
23
23
 
24
24
  ProgressCallback = Callable[[int, int], None]
@@ -1532,119 +1532,6 @@ def _pma_upload_amazon_callback(bytes_read, total_size, previous, filename):
1532
1532
  return v
1533
1533
 
1534
1534
 
1535
- def upload(local_source_slide, target_folder, target_pma_core_sessionID, callback=None, verify=True):
1536
- """
1537
- Uploads a slide to a PMA.core server. Requires a PMA.start installation
1538
- :param str local_source_slide: The local PMA.start relative file to upload
1539
- :param str target_folder: The root directory and path to upload to the PMA.core server
1540
- :param str target_pma_core_sessionID: A valid session id for a PMA.core server
1541
- :param function|boolean callback: If True a default progress will be printed.
1542
- If a function is passed it will be called for progress on each file upload.
1543
- The function has the following signature:
1544
- `callback(bytes_read, bytes_length, filename)`
1545
- """
1546
- if not _pma_is_lite():
1547
- raise Exception(
1548
- "No PMA.start found on localhost. Are you sure it is running?")
1549
-
1550
- if not target_folder:
1551
- raise ValueError("target_folder cannot be empty")
1552
-
1553
- if (target_folder.startswith("/")):
1554
- target_folder = target_folder[1:]
1555
-
1556
- files = get_files_for_slide(local_source_slide, _pma_pmacoreliteSessionID)
1557
- sessionID = _pma_session_id(target_pma_core_sessionID)
1558
- url = _pma_url(sessionID) + "transfer/Upload?sessionID=" + pma._pma_q(sessionID)
1559
-
1560
- mainDirectory = ''
1561
- for i, f in enumerate(files):
1562
- md = os.path.dirname(f)
1563
- if i == 0 or len(md) < len(mainDirectory):
1564
- mainDirectory = md
1565
-
1566
- uploadFiles = []
1567
- for i, filepath in enumerate(files):
1568
- s = os.path.getsize(filepath)
1569
- if s > 0:
1570
- uploadFiles.append({
1571
- "Path": filepath.replace(mainDirectory, '').strip("\\").strip('/'),
1572
- "Length": s,
1573
- "IsMain": i == len(files) - 1,
1574
- "FullPath": filepath
1575
- })
1576
-
1577
- data = {"Path": target_folder, "Files": uploadFiles}
1578
-
1579
- uploadHeaderResponse = requests.post(url, json=data, verify=verify)
1580
- if not uploadHeaderResponse.status_code == 200:
1581
- print(uploadHeaderResponse.json())
1582
- raise Exception(uploadHeaderResponse.json()["Message"])
1583
-
1584
- uploadHeader = uploadHeaderResponse.json()
1585
-
1586
- pmaCoreUploadUrl = _pma_url(sessionID) + "transfer/Upload/" + pma._pma_q(
1587
- uploadHeader["Id"]) + "?sessionID=" + pma._pma_q(sessionID) + "&path={0}"
1588
-
1589
- isAmazonUpload = True
1590
- if not uploadHeader['Urls']:
1591
- isAmazonUpload = False
1592
- uploadHeader['Urls'] = [pmaCoreUploadUrl.format(
1593
- f["Path"]) for f in uploadFiles]
1594
-
1595
- for i, f in enumerate(uploadFiles):
1596
- uploadUrl = uploadHeader['Urls'][i]
1597
-
1598
- e = MultipartEncoder(
1599
- fields={"file": (os.path.basename(f["Path"]), open(f["FullPath"], 'rb'), 'application/octet-stream')})
1600
-
1601
- _callback = None
1602
- if callback is True:
1603
- print("Uploading file: {0}".format(e.fields["file"][0]))
1604
- if not isAmazonUpload:
1605
- def _callback(x):
1606
- return _pma_upload_callback(
1607
- monitor, e.fields["file"][0])
1608
- else:
1609
- def _callback(bytes_read, total_size, previous):
1610
- return _pma_upload_amazon_callback(
1611
- bytes_read, total_size, previous, e.fields["file"][0])
1612
- elif callable(callback):
1613
- def _callback(x):
1614
- return callback(
1615
- x.bytes_read, x.len, x.previous, e.fields["file"][0])
1616
-
1617
- monitor = MultipartEncoderMonitor(e, _callback)
1618
-
1619
- monitor.previous = 0
1620
-
1621
- r = None
1622
- if not isAmazonUpload:
1623
- r = requests.post(uploadUrl, data=monitor, headers={
1624
- 'Content-Type': monitor.content_type},
1625
- verify=verify)
1626
- else:
1627
- headers = {'Content-Length': str(f["Length"])}
1628
- if uploadHeader['UploadType'] == 2:
1629
- headers = {
1630
- 'Content-Length': str(f["Length"]), 'x-ms-blob-type': 'BlockBlob'}
1631
-
1632
- r = requests.put(uploadUrl, data=UploadChunksIterator(
1633
- open(f["FullPath"], 'rb'), f["Path"], f["Length"], _callback), headers=headers, verify=verify)
1634
-
1635
- if r.status_code < 200 or r.status_code >= 300:
1636
- raise Exception("Error uploading file {0}: {1} \r\n{2}: {3}".format(
1637
- f["Path"], uploadUrl, r.status_code, r.text))
1638
-
1639
- uploadFinalizeResponse = requests.get(_pma_url(sessionID) + "transfer/Upload/"
1640
- + pma._pma_q(uploadHeader["Id"]) + "?sessionID=" + pma._pma_q(sessionID),
1641
- verify=verify)
1642
- if uploadFinalizeResponse.status_code < 200 or uploadFinalizeResponse.status_code >= 300:
1643
- print(uploadFinalizeResponse.json())
1644
- raise Exception(uploadFinalizeResponse.json()[
1645
- "Message"] + uploadFinalizeResponse.json()["ExceptionMessage"])
1646
-
1647
-
1648
1535
  class UploadChunksIterator:
1649
1536
  def __init__(self, file: io.BufferedReader, filename, total_size: int, callback, chunk_size: int = 16 * 1024):
1650
1537
  self.file = file
@@ -1663,11 +1550,10 @@ class UploadChunksIterator:
1663
1550
 
1664
1551
  if not data:
1665
1552
  raise StopIteration
1666
- self.bytes_read += self.chunk_size
1667
- if self.callback is not None:
1668
- v = self.callback(self.bytes_read, self.total_size, self.previous)
1669
- if v is not None:
1670
- self.previous = v
1553
+ self.bytes_read += len(data)
1554
+ if self.callback:
1555
+ self.callback(self.bytes_read, self.total_size)
1556
+
1671
1557
  return data
1672
1558
 
1673
1559
  def __len__(self):
@@ -2008,10 +1894,256 @@ def get_annotation_distance(slideRef, layerID, annotationID, sessionID=None, ver
2008
1894
  return r.text
2009
1895
 
2010
1896
 
2011
- #********************************#
2012
- # === Large Files Uploader === #
2013
- #********************************#
2014
- async def upload_large_files(
1897
+ # **************************************#
1898
+ # === Uploader === #
1899
+ # **************************************#
1900
+ FIVE_GB = 5 * 1024 * 1024 * 1024
1901
+ async def upload(
1902
+ *,
1903
+ pma_core_url: str,
1904
+ session_id: str,
1905
+ slide_path: str,
1906
+ upload_directory: str,
1907
+ progress_callback=None,
1908
+ ):
1909
+ """
1910
+ Main upload entry point.
1911
+
1912
+ Automatically:
1913
+ - calculates full slide size (file + folder + nested files)
1914
+ - detects server upload type
1915
+ - routes upload to legacy transfer or large file uploader
1916
+ """
1917
+
1918
+ if not os.path.isfile(slide_path):
1919
+ return False, f"File not found: {slide_path}"
1920
+
1921
+ # ================================
1922
+ # CALCULATE TOTAL SLIDE SIZE
1923
+ # ================================
1924
+ total_size = 0
1925
+
1926
+ # main file size
1927
+ total_size += os.stat(slide_path).st_size
1928
+
1929
+ # possible slide folder (multi-files slides)
1930
+ slide_name_no_ext = os.path.splitext(os.path.basename(slide_path))[0]
1931
+ slide_dir = os.path.join(os.path.dirname(slide_path), slide_name_no_ext)
1932
+
1933
+ if os.path.isdir(slide_dir):
1934
+ for root, _, files in os.walk(slide_dir):
1935
+ for f in files:
1936
+ total_size += os.stat(os.path.join(root, f)).st_size
1937
+
1938
+ print("Total slide size:", round(total_size / (1024 ** 3), 2), "GB")
1939
+
1940
+ # ================================
1941
+ # CHECK UPLOAD TYPE (PRE-FLIGHT)
1942
+ # ================================
1943
+
1944
+ try:
1945
+ client = PmaCoreClient(pma_core_url)
1946
+
1947
+ file_size = os.stat(slide_path).st_size
1948
+ file_name = os.path.basename(slide_path)
1949
+
1950
+ header = UploadHeaderModel(
1951
+ path=upload_directory,
1952
+ files=[
1953
+ UploadFileModel(
1954
+ isMain=True,
1955
+ length=file_size,
1956
+ path=file_name
1957
+ )
1958
+ ]
1959
+ )
1960
+
1961
+ resp = await client.upload_header(header, session_id)
1962
+
1963
+ print("Detected UploadType:", resp.upload_type)
1964
+
1965
+ # ================================
1966
+ # ROUTING LOGIC
1967
+ # ================================
1968
+
1969
+ # UploadType 0 → always small uploader
1970
+ if resp.upload_type == 0:
1971
+ print("UploadType 0 → forcing SMALL uploader")
1972
+
1973
+ return upload_legacy(
1974
+ session_id=session_id,
1975
+ slide_path=slide_path,
1976
+ upload_directory=upload_directory,
1977
+ progress_callback=progress_callback
1978
+ )
1979
+
1980
+ # Otherwise fallback to size logic
1981
+ if total_size < FIVE_GB:
1982
+ print("Using SMALL file uploader")
1983
+
1984
+ return upload_legacy(
1985
+ session_id=session_id,
1986
+ slide_path=slide_path,
1987
+ upload_directory=upload_directory,
1988
+ progress_callback=progress_callback
1989
+ )
1990
+
1991
+ else:
1992
+ print("Using LARGE file uploader")
1993
+
1994
+ return await upload_file_over_5gb(
1995
+ pma_core_url=pma_core_url,
1996
+ session_id=session_id,
1997
+ slide_path=slide_path,
1998
+ upload_directory=upload_directory,
1999
+ progress_callback=progress_callback
2000
+ )
2001
+
2002
+ except Exception as e:
2003
+ return False, str(e)
2004
+
2005
+ # **************************************#
2006
+ # === Small Files < 5bg Uploader === #
2007
+ # **************************************#
2008
+ def upload_legacy(slide_path, upload_directory, session_id, progress_callback=None, verify=True):
2009
+ """
2010
+ Synchronous upload implementation using direct HTTP requests.
2011
+
2012
+ Handles:
2013
+ - filesystem uploads (any file size when UploadType == 0)
2014
+ - non-chunked cloud uploads
2015
+ - single-file and multi-file slides (MRXS, VSI, etc.)
2016
+ - global progress aggregation across all uploaded files
2017
+ """
2018
+
2019
+ if not upload_directory:
2020
+ raise ValueError("target_folder cannot be empty")
2021
+
2022
+ if (upload_directory.startswith("/")):
2023
+ upload_directory = upload_directory[1:]
2024
+
2025
+ files = get_files_for_slide(slide_path, _pma_pmacoreliteSessionID)
2026
+ sessionID = _pma_session_id(session_id)
2027
+ url = _pma_url(sessionID) + "transfer/Upload?sessionID=" + pma._pma_q(sessionID)
2028
+
2029
+ mainDirectory = ''
2030
+ for i, f in enumerate(files):
2031
+ md = os.path.dirname(f)
2032
+ if i == 0 or len(md) < len(mainDirectory):
2033
+ mainDirectory = md
2034
+
2035
+ uploadFiles = []
2036
+ for i, filepath in enumerate(files):
2037
+ s = os.path.getsize(filepath)
2038
+ if s > 0:
2039
+ uploadFiles.append({
2040
+ "Path": filepath.replace(mainDirectory, '').strip("\\").strip('/'),
2041
+ "Length": s,
2042
+ "IsMain": i == len(files) - 1,
2043
+ "FullPath": filepath
2044
+ })
2045
+ # ==============================
2046
+ # GLOBAL PROGRESS (ALL FILES)
2047
+ # ==============================
2048
+ total_bytes = sum(x["Length"] for x in uploadFiles) if uploadFiles else 0
2049
+ sent_total = 0
2050
+
2051
+ # If the user passed progress_callback=True → enable the default print
2052
+ if progress_callback is True:
2053
+ def progress_callback(sent: int, total: int):
2054
+ if total > 0:
2055
+ percent = sent / total * 100
2056
+ print(f"Upload progress: {percent:.2f}%")
2057
+
2058
+ def report_delta(delta: int):
2059
+ nonlocal sent_total
2060
+ if delta <= 0:
2061
+ return
2062
+ sent_total += delta
2063
+ if callable(progress_callback) and total_bytes > 0:
2064
+ if sent_total > total_bytes:
2065
+ sent_total = total_bytes
2066
+ progress_callback(sent_total, total_bytes)
2067
+
2068
+ data = {"Path": upload_directory, "Files": uploadFiles}
2069
+
2070
+ uploadHeaderResponse = requests.post(url, json=data, verify=verify)
2071
+ if not uploadHeaderResponse.status_code == 200:
2072
+ print(uploadHeaderResponse.json())
2073
+ raise Exception(uploadHeaderResponse.json()["Message"])
2074
+
2075
+ uploadHeader = uploadHeaderResponse.json()
2076
+
2077
+ pmaCoreUploadUrl = _pma_url(sessionID) + "transfer/Upload/" + pma._pma_q(
2078
+ uploadHeader["Id"]) + "?sessionID=" + pma._pma_q(sessionID) + "&path={0}"
2079
+
2080
+ isAmazonUpload = True
2081
+ if not uploadHeader['Urls']:
2082
+ isAmazonUpload = False
2083
+ uploadHeader['Urls'] = [pmaCoreUploadUrl.format(
2084
+ f["Path"]) for f in uploadFiles]
2085
+
2086
+ for i, f in enumerate(uploadFiles):
2087
+ uploadUrl = uploadHeader['Urls'][i]
2088
+
2089
+ e = MultipartEncoder(
2090
+ fields={"file": (os.path.basename(f["Path"]), open(f["FullPath"], 'rb'), 'application/octet-stream')})
2091
+
2092
+ # ==============================
2093
+ # PER-FILE -> GLOBAL PROGRESS
2094
+ # ==============================
2095
+ last_sent = 0
2096
+
2097
+ def monitor_callback(monitor):
2098
+ nonlocal last_sent
2099
+ current = monitor.bytes_read
2100
+ delta = current - last_sent
2101
+ last_sent = current
2102
+ report_delta(delta)
2103
+
2104
+ def iterator_callback(bytes_read, _total_size):
2105
+ nonlocal last_sent
2106
+ current = bytes_read
2107
+ delta = current - last_sent
2108
+ last_sent = current
2109
+ report_delta(delta)
2110
+
2111
+ monitor = MultipartEncoderMonitor(e, monitor_callback)
2112
+
2113
+ monitor.previous = 0
2114
+
2115
+ r = None
2116
+ if not isAmazonUpload:
2117
+ r = requests.post(uploadUrl, data=monitor, headers={
2118
+ 'Content-Type': monitor.content_type},
2119
+ verify=verify)
2120
+ else:
2121
+ headers = {'Content-Length': str(f["Length"])}
2122
+ if uploadHeader['UploadType'] == 2:
2123
+ headers = {
2124
+ 'Content-Length': str(f["Length"]), 'x-ms-blob-type': 'BlockBlob'}
2125
+
2126
+ r = requests.put(uploadUrl, data=UploadChunksIterator(
2127
+ open(f["FullPath"], 'rb'), f["Path"], f["Length"], iterator_callback), headers=headers, verify=verify)
2128
+
2129
+ if r.status_code < 200 or r.status_code >= 300:
2130
+ raise Exception("Error uploading file {0}: {1} \r\n{2}: {3}".format(
2131
+ f["Path"], uploadUrl, r.status_code, r.text))
2132
+
2133
+ uploadFinalizeResponse = requests.get(_pma_url(sessionID) + "transfer/Upload/"
2134
+ + pma._pma_q(uploadHeader["Id"]) + "?sessionID=" + pma._pma_q(sessionID),
2135
+ verify=verify)
2136
+ if uploadFinalizeResponse.status_code < 200 or uploadFinalizeResponse.status_code >= 300:
2137
+ print(uploadFinalizeResponse.json())
2138
+ raise Exception(uploadFinalizeResponse.json()[
2139
+ "Message"] + uploadFinalizeResponse.json()["ExceptionMessage"])
2140
+ return True, None
2141
+
2142
+
2143
+ # **************************************#
2144
+ # === Large Files > 5gb Uploader === #
2145
+ # **************************************#
2146
+ async def upload_file_over_5gb(
2015
2147
  *,
2016
2148
  pma_core_url: str,
2017
2149
  session_id: str,
@@ -2019,11 +2151,18 @@ async def upload_large_files(
2019
2151
  upload_directory: str,
2020
2152
  progress_callback=None,
2021
2153
  ):
2154
+ """
2155
+ Asynchronous large-file upload implementation using PmaCoreClient.
2156
+
2157
+ Handles:
2158
+ - chunked cloud uploads
2159
+ - multi-file slide stacks
2160
+ - global progress tracking across all uploaded files
2161
+ """
2022
2162
 
2023
2163
  # ===== GETTING ALL FILES =====
2024
2164
  all_files = []
2025
2165
 
2026
- # MAIN VSI
2027
2166
  all_files.append(slide_path)
2028
2167
 
2029
2168
  vsi_name_no_ext = os.path.splitext(os.path.basename(slide_path))[0]
@@ -2073,7 +2212,7 @@ async def upload_large_files(
2073
2212
  )
2074
2213
 
2075
2214
  if not ok:
2076
- return False, f"VSI upload failed: {err}"
2215
+ return False, f"Upload failed: {err}"
2077
2216
 
2078
2217
  # ===== STACK =====
2079
2218
  if os.path.isdir(local_stack_root):
@@ -2105,6 +2244,7 @@ async def upload_large_files(
2105
2244
 
2106
2245
  return True, None
2107
2246
 
2247
+
2108
2248
  async def large_files_upload_package(
2109
2249
  *,
2110
2250
  pma_core_url: str,
@@ -2113,6 +2253,15 @@ async def large_files_upload_package(
2113
2253
  upload_directory: str,
2114
2254
  progress_callback=None,
2115
2255
  ) -> tuple[bool, str | None]:
2256
+ """
2257
+ Low-level async upload routine for a single file.
2258
+
2259
+ Performs:
2260
+ - upload header request
2261
+ - multipart or direct upload
2262
+ - upload completion polling
2263
+ """
2264
+
2116
2265
  try:
2117
2266
  client = PmaCoreClient(pma_core_url)
2118
2267
  print("trust_env:", client.session.trust_env)
@@ -2138,11 +2287,11 @@ async def large_files_upload_package(
2138
2287
  # ===== SEND HEADER =====
2139
2288
  resp = await client.upload_header(header, session_id)
2140
2289
 
2141
- print("UploadType:", resp.upload_type)
2290
+ # print("UploadType:", resp.upload_type)
2142
2291
  print("MultipartFiles:", resp.multipart_files)
2143
2292
  print("Urls:", resp.urls)
2144
2293
 
2145
- # ===== MULTIPART =====
2294
+ # ===== MULTIPART ONLY IF SERVER SENT PARTS =====
2146
2295
  if resp.multipart_files and len(resp.multipart_files) > 0:
2147
2296
 
2148
2297
  mp = resp.multipart_files[0]
@@ -2152,7 +2301,6 @@ async def large_files_upload_package(
2152
2301
  )
2153
2302
 
2154
2303
  with open(slide_path, "rb") as stream:
2155
-
2156
2304
  await client.upload_multipart_file_to_s3(
2157
2305
  mp,
2158
2306
  full_virtual_path,
@@ -2161,13 +2309,12 @@ async def large_files_upload_package(
2161
2309
  progress_callback=progress_callback
2162
2310
  )
2163
2311
 
2164
- # ===== SINGLE PUT =====
2312
+ # ===== SINGLE PUT / AZURE / FILESYSTEM =====
2165
2313
  else:
2166
2314
 
2167
2315
  upload_url = resp.urls[0] if resp.urls else None
2168
2316
 
2169
2317
  with open(slide_path, "rb") as stream:
2170
-
2171
2318
  await client.upload_file(
2172
2319
  resp.id,
2173
2320
  resp.upload_type,
@@ -2193,7 +2340,14 @@ async def large_files_upload_package(
2193
2340
  except Exception as e:
2194
2341
  return False, str(e)
2195
2342
 
2343
+
2196
2344
  # callback for large files upload
2197
2345
  def on_progress(bytes_sent, total_bytes):
2346
+ """
2347
+ Default progress callback.
2348
+
2349
+ Prints upload percentage based on transferred bytes.
2350
+ """
2351
+
2198
2352
  percent = bytes_sent / total_bytes * 100
2199
2353
  print(f"Upload progress: {percent:.2f}%")
@@ -11,12 +11,10 @@ from urllib3.poolmanager import PoolManager
11
11
 
12
12
 
13
13
  UploadProgressCallback = Callable[[int, int], None]
14
- # args: bytes_sent, total_bytes
15
14
 
16
15
  class TLS12HttpAdapter(HTTPAdapter):
17
16
  def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
18
17
  ctx = ssl.create_default_context()
19
- # Принудительно TLS 1.2 (часто лечит EOF на прокси/антивирусе)
20
18
  ctx.minimum_version = ssl.TLSVersion.TLSv1_2
21
19
  ctx.maximum_version = ssl.TLSVersion.TLSv1_2
22
20
 
@@ -176,12 +174,10 @@ class PmaCoreClient:
176
174
  if not self.server_url.endswith('/'):
177
175
  self.server_url += '/'
178
176
 
179
- # --- FIX: не использовать proxy/SSL-intercept из окружения ---
180
177
  self.session = requests.Session()
181
- self.session.trust_env = False # <- ключевое: игнорировать HTTP(S)_PROXY и т.п.
178
+ self.session.trust_env = False
182
179
  self.session.mount("https://", TLS12HttpAdapter())
183
180
 
184
-
185
181
  def authenticate(self, username: str, password: str, caller: str) -> AuthenticateResponse:
186
182
  service_url = urljoin(self.server_url, 'api/json/Authenticate')
187
183
  params = {'username': username, 'password': password, 'caller': caller}
@@ -209,8 +205,13 @@ class PmaCoreClient:
209
205
  total_bytes: int | None = None,
210
206
  progress_callback: UploadProgressCallback | None = None):
211
207
 
212
- if upload_type == "Azure":
213
- await self.upload_file_to_azure(upload_url, stream)
208
+ if upload_type == "Azure" or upload_type == 2:
209
+ await self.upload_file_to_azure(
210
+ upload_url,
211
+ stream,
212
+ total_bytes=total_bytes,
213
+ progress_callback=progress_callback,
214
+ )
214
215
  return
215
216
 
216
217
  upload_to_presigned = bool(upload_url)
@@ -248,11 +249,9 @@ class PmaCoreClient:
248
249
 
249
250
  return
250
251
 
251
- # upload через сервер (multipart form)
252
252
  url = f"{self.server_url}transfer/Upload/{upload_id}"
253
253
  params = {"sessionId": session_id, "path": relative_path}
254
254
 
255
- # важно: wrapped передаём как file handle в files
256
255
  files = {"file": (relative_path.split("/")[-1], wrapped, "application/octet-stream")}
257
256
  response = await asyncio.to_thread(
258
257
  requests.post,
@@ -285,7 +284,7 @@ class PmaCoreClient:
285
284
  params = {'sessionId': session_id}
286
285
  payload = {
287
286
  "FilePath": relative_path,
288
- "UploadId": multipart_info.upload_id, # см. пункт 6 ниже
287
+ "UploadId": multipart_info.upload_id,
289
288
  "Parts": [{"PartNumber": p.PartNumber, "ETag": p.ETag} for p in e_tags],
290
289
  }
291
290
  headers = {'Content-Type': 'application/json'}
@@ -299,9 +298,21 @@ class PmaCoreClient:
299
298
  )
300
299
  response.raise_for_status()
301
300
 
302
- async def upload_file_to_azure(self, upload_url: str, stream: IO):
301
+ async def upload_file_to_azure(
302
+ self,
303
+ upload_url: str,
304
+ stream: IO,
305
+ total_bytes: int | None = None,
306
+ progress_callback: UploadProgressCallback | None = None,
307
+ ):
303
308
  block_ids: List[str] = []
304
- await self.upload_blocks_to_azure(block_ids, upload_url, stream)
309
+ await self.upload_blocks_to_azure(
310
+ block_ids,
311
+ upload_url,
312
+ stream,
313
+ total_bytes=total_bytes,
314
+ progress_callback=progress_callback,
315
+ )
305
316
  headers = {
306
317
  'x-ms-blob-content-type': 'application/octet-stream',
307
318
  'Connection': 'Keep-Alive',
@@ -321,11 +332,17 @@ class PmaCoreClient:
321
332
  response = requests.put(commit_url, data=body, headers=headers)
322
333
  response.raise_for_status()
323
334
 
324
- async def upload_blocks_to_azure(self, block_ids: List[str], upload_url: str, stream: IO):
325
- block_size = 4 * 1024 * 1024 # 4MB как в Java
335
+ async def upload_blocks_to_azure(
336
+ self,
337
+ block_ids: List[str],
338
+ upload_url: str,
339
+ stream: IO,
340
+ total_bytes: int | None = None,
341
+ progress_callback: UploadProgressCallback | None = None,
342
+ ):
343
+ block_size = 4 * 1024 * 1024
326
344
  block_id = 0
327
-
328
- # если в upload_url есть пробелы/и т.п.
345
+ sent_total = 0
329
346
  safe_base = upload_url.replace(" ", "%20")
330
347
 
331
348
  while True:
@@ -333,21 +350,19 @@ class PmaCoreClient:
333
350
  if not chunk:
334
351
  break
335
352
 
353
+ sent_total += len(chunk)
354
+ if progress_callback and total_bytes:
355
+ progress_callback(sent_total, total_bytes)
356
+
336
357
  plain = (f"block_{block_id:06d}").encode("utf-8")
337
358
  import base64
338
359
  base64_block_id = base64.b64encode(plain).decode("utf-8")
339
-
340
- # ВАЖНО: blockid обязательно URL-encode
341
360
  encoded_block_id = quote(base64_block_id, safe="")
342
-
343
361
  block_upload_url = f"{safe_base}&comp=block&blockid={encoded_block_id}"
344
362
 
345
- # Для block PUT не надо x-ms-blob-type (особенно если URL — SAS)
346
363
  headers = {
347
364
  "Connection": "Keep-Alive",
348
365
  "Keep-Alive": "3600",
349
- # при необходимости можно добавить x-ms-version, если требует аккаунт:
350
- # "x-ms-version": "2020-10-02",
351
366
  }
352
367
 
353
368
  response = await asyncio.to_thread(
@@ -358,7 +373,7 @@ class PmaCoreClient:
358
373
  )
359
374
  response.raise_for_status()
360
375
 
361
- block_ids.append(base64_block_id) # В список блоков кладём original base64 id (как в Java)
376
+ block_ids.append(base64_block_id)
362
377
  block_id += 1
363
378
 
364
379
  def _iter_file_range(self, stream: IO, start: int, length: int, buf_size: int = 8 * 1024 * 1024):
@@ -0,0 +1 @@
1
+ __version__ = '2.0.0.181'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pma_python
3
- Version: 2.0.0.179
3
+ Version: 2.0.0.181
4
4
  Summary: Universal viewing of digital microscopy, whole slide imaging and digital pathology data
5
5
  Home-page: https://github.com/pathomation/pma_python
6
6
  Author: Pathomation
@@ -1 +0,0 @@
1
- __version__ = '2.0.0.179'
File without changes
File without changes
File without changes