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.
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/PKG-INFO +1 -1
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/core.py +284 -130
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/pma_core_client.py +38 -23
- pma_python-2.0.0.181/pma_python/version.py +1 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/PKG-INFO +1 -1
- pma_python-2.0.0.179/pma_python/version.py +0 -1
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/README.md +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/long_desc.txt +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/__init__.py +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/control.py +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/core_admin.py +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/pma.py +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python/view.py +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/SOURCES.txt +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/dependency_links.txt +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/not-zip-safe +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/requires.txt +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/pma_python.egg-info/top_level.txt +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/setup.cfg +0 -0
- {pma_python-2.0.0.179 → pma_python-2.0.0.181}/setup.py +0 -0
|
@@ -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 +=
|
|
1667
|
-
if self.callback
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
#
|
|
2013
|
-
|
|
2014
|
-
|
|
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"
|
|
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
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
325
|
-
|
|
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)
|
|
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 +0,0 @@
|
|
|
1
|
-
__version__ = '2.0.0.179'
|
|
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
|
|
File without changes
|