├── .gitignore ├── README.md ├── long_desc.txt ├── pma_python ├── __init__.py ├── control.py ├── core.py ├── core_admin.py ├── pma.py ├── version.py └── view.py ├── samples ├── ConvertToTiff.py ├── jupyter │ ├── ConvertToTiff.ipynb │ ├── PMA.core.admin.ipynb │ ├── PMA.core.ipynb │ ├── PMA.start.ipynb │ └── PMA.view.ipynb └── test.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.ipynb_checkpoints/ 3 | build/ 4 | dist/ 5 | pma_python\.egg-info/ 6 | venv/ 7 | /samples/ 8 | setup.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pma_python 2 | pma_python is a Python wrapper library for PMA.start (http://free.pathomation.com), 3 | a universal viewer by Pathomation for whole slide imaging and microscopy. 4 | 5 | YOU NEED TO HAVE PMA.START (OR PMA.CORE) RUNNING IN ORDER TO USE THIS 6 | LIBRARY. PMA_PYTHON IS NOT A STAND-ALONE LIBRARY FOR WSI. 7 | 8 | If you're working with 9 | 10 | * microscopy data and are you tired of having to use a different 11 | library for each vendor specific digital slide format 12 | * image analysis software and are you looking for a way to have 13 | complete control over your algorithms (and not just be able to 14 | select those provided for you) 15 | * novel evaluation methods for histology / pathology and do you 16 | just wish there was a way to now automatically evaluate it on 17 | 100s of slides (batch processing) 18 | 19 | and you're doing all of this in Python, then PMA.start and 20 | pma_python are for you! 21 | 22 | PMA.start can be downloaded free of charge from http://free.pathomation.com 23 | 24 | With pma_python, you can inspect and navigate any type of 25 | microscopic imaging file format available. Whether you have 26 | macroscopic observations, whole slide imaging data, or fluorescent 27 | snapshot observations, we can bring it all into Python now. 28 | 29 | The following vendor formats are currently supported: 30 | 31 | * TIFF (.tif, .tiff) with JPEG, JPEG2000, LZW, Deflate, Raw, RLE 32 | * JPEG (.jpeg, .jpg) 33 | * JPEG 2000 (.jp2) 34 | * PNG (.png) 35 | * Olympus VSI (.vsi) with lossless JPEG, JPEG, Raw 36 | * Ventana / Roche BIF (.bif) 37 | * Hamamatsu (.vms, .ndpi, .dcm) with JPEG, JPEG2000 38 | * Huron Technologies (.tif) 39 | * 3DHistech (.mrxs) with JPEG, PNG, BMP 40 | * Aperio / Leica (.svs, .cws, .scn, .lif, DICOMDIR) with JPEG, JPEG2000 41 | * Carl Zeiss (.zvi, .czi, .lsm) with Raw, PNG, JPEG, LZW, Deflate, JPEG2000 42 | * Open Microscopy Environment OME-TIFF (.tf2, .tf8, .btf, .ome.tif) 43 | * Nikon (.nd2, .tiff) with Deflate, JPEG, JPEG2000, LZW, Deflate, Raw, RLE 44 | * Philips (.tif) with JPEG, JPEG2000, LZW, Deflate, Raw, RLE 45 | * Sakura (.svslide) with JPEG(sqlite2, sqlite3, mssql) 46 | * Menarini (.ini) with Raw 47 | * Motic (.mds) 48 | * Zoomify (.zif) with JPEG, JPEG2000, LZW, Deflate, Raw, RLE 49 | * SmartZoom (.szi) with JPEG, BMP 50 | * Objective Imaging / Glissano (.sws) 51 | * Perkin Elmer (.qptiff) 52 | 53 | The most up to date list with supported file formats can be found at 54 | http://free.pathomation.com/formats 55 | 56 | ## Installation from source 57 | To install from pypi 58 | ```sh 59 | pip install pma_python 60 | ``` 61 | To upgrade an already existing version from pypi 62 | ```sh 63 | pip install --upgrade pma_python 64 | ``` 65 | 66 | ## How to use 67 | ```python 68 | >>> from pma_python import * 69 | ``` 70 | -------------------------------------------------------------------------------- /long_desc.txt: -------------------------------------------------------------------------------- 1 | pma_python is a Python wrapper library for PMA.start (http://free.pathomation.com), 2 | a universal viewer by Pathomation for whole slide imaging and microscopy. 3 | 4 | YOU NEED TO HAVE PMA.START (OR PMA.CORE) RUNNING IN ORDER TO USE THIS 5 | LIBRARY. PMA_PYTHON IS NOT A STAND-ALONE LIBRARY FOR WSI. 6 | 7 | If you're working with 8 | 9 | * microscopy data and are you tired of having to use a different library for each vendor specific digital slide format 10 | * image analysis software and are you looking for a way to have complete control over your algorithms (and not just be able to select those provided for you) 11 | * novel evaluation methods for histology / pathology and do you just wish there was a way to now automatically evaluate it on 100s of slides (batch processing) 12 | 13 | and you're doing all of this in Python, then PMA.start and 14 | pma_python are for you! 15 | 16 | PMA.start can be downloaded free of charge from http://free.pathomation.com 17 | 18 | With pma_python, you can inspect and navigate any type of 19 | microscopic imaging file format available. Whether you have 20 | macroscopic observations, whole slide imaging data, or fluorescent 21 | snapshot observations, we can bring it all into Python now. 22 | 23 | The following vendor formats are currently supported: 24 | 25 | * TIFF (.tif, .tiff) with JPEG, JPEG2000, LZW, Deflate, Raw, RLE 26 | * JPEG (.jpeg, .jpg) 27 | * JPEG 2000 (.jp2) 28 | * PNG (.png) 29 | * Olympus VSI (.vsi) with lossless JPEG, JPEG, Raw 30 | * Ventana / Roche BIF (.bif) 31 | * Hamamatsu (.vms, .ndpi, .dcm) with JPEG, JPEG2000 32 | * Huron Technologies (.tif) 33 | * 3DHistech (.mrxs) with JPEG, PNG, BMP 34 | * Aperio / Leica (.svs, .cws, .scn, .lif, DICOMDIR) with JPEG, JPEG2000 35 | * Carl Zeiss (.zvi, .czi, .lsm) with Raw, PNG, JPEG, LZW, Deflate, JPEG2000 36 | * Open Microscopy Environment OME-TIFF (.tf2, .tf8, .btf, .ome.tif) 37 | * Nikon (.nd2, .tiff) with Deflate, JPEG, JPEG2000, LZW, Deflate, Raw, RLE 38 | * Philips (.tif) with JPEG, JPEG2000, LZW, Deflate, Raw, RLE 39 | * Sakura (.svslide) with JPEG(sqlite2, sqlite3, mssql) 40 | * Menarini (.ini) with Raw 41 | * Motic (.mds) 42 | * Zoomify (.zif) with JPEG, JPEG2000, LZW, Deflate, Raw, RLE 43 | * SmartZoom (.szi) with JPEG, BMP 44 | * Objective Imaging / Glissano (.sws) 45 | * Perkin Elmer (.qptiff) 46 | 47 | The most up to date list with supported file formats can be found at 48 | http://free.pathomation.com/formats 49 | 50 | -------------------------------------------------------------------------------- /pma_python/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import * 2 | from .pma import * 3 | from .core import * 4 | from .core_admin import * 5 | from .control import * 6 | from .view import * 7 | -------------------------------------------------------------------------------- /pma_python/control.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.error 3 | from urllib import request, parse 4 | from pma_python import core, pma 5 | 6 | import requests 7 | 8 | __version__ = pma.__version__ 9 | 10 | pma_training_session_role_supervisor = 1 11 | pma_training_session_role_trainee = 2 12 | pma_training_session_role_observer = 3 13 | 14 | pma_interaction_mode_locked = 0 15 | pma_interaction_mode_test_active = 1 16 | pma_interaction_mode_review = 2 17 | pma_interaction_mode_consensus_view = 3 18 | pma_interaction_mode_browse = 4 19 | pma_interaction_mode_board = 5 20 | pma_interaction_mode_consensus_score_edit = 6 21 | pma_interaction_mode_self_review = 7 22 | pma_interaction_mode_self_test = 8 23 | pma_interaction_mode_hidden = 9 24 | pma_interaction_mode_clinical_information_edit = 10 25 | 26 | 27 | def set_debug_flag(flag): 28 | """ 29 | Determine whether pma_python module runs in debugging mode or not. 30 | When in debugging mode (flag = true), extra output is produced when certain conditions in the code are not met 31 | """ 32 | pma._pma_set_debug_flag(flag) 33 | 34 | 35 | def get_version_info(pmacontrolURL): 36 | """ 37 | Get version info from PMA.control instance running at pmacontrolURL 38 | """ 39 | # why? because GetVersionInfo can be invoked WITHOUT a valid SessionID; _pma_api_url() takes session information into account 40 | url = pma._pma_join(pmacontrolURL, "api/version") 41 | try: 42 | headers = {'Accept': 'application/json'} 43 | r = pma._pma_http_get(url, headers) 44 | except Exception as e: 45 | print(e) 46 | return None 47 | return r.json() 48 | 49 | 50 | def _pma_get_training_sessions(pmacontrolURL, pmacoreSessionID): 51 | """ 52 | Retrieve a list of currently defined training sessions in PMA.control. 53 | """ 54 | url = pma._pma_join( 55 | pmacontrolURL, "api/Sessions?sessionID=" + pma._pma_q(pmacoreSessionID)) 56 | try: 57 | headers = {'Accept': 'application/json'} 58 | r = pma._pma_http_get(url, headers) 59 | except Exception as e: 60 | print(e) 61 | return None 62 | return r.json() 63 | 64 | 65 | def _pma_format_training_session_properly(sess): 66 | """ 67 | Helper method to convert a JSON representation of a PMA.control training session to a proper Python-esque structure 68 | """ 69 | sess_data = { 70 | "Id": sess["Id"], 71 | "Title": sess["Title"], 72 | "LogoPath": sess["LogoPath"], 73 | "StartsOn": sess["StartsOn"], 74 | "EndsOn": sess["EndsOn"], 75 | "ProjectId": sess["ProjectId"], 76 | "State": sess["State"], 77 | "CaseCollections": {}, 78 | "NumberOfParticipants": len(sess["Participants"]) 79 | } 80 | for coll in sess["CaseCollections"]: 81 | sess_data["CaseCollections"][coll["CaseCollectionId"]] = { 82 | "Title": coll["Title"], "Url": coll["Url"]} 83 | 84 | return sess_data 85 | 86 | 87 | def get_training_sessions_for_participant(pmacontrolURL, participantUsername, pmacoreSessionID): 88 | full_training_sessions = _pma_get_training_sessions( 89 | pmacontrolURL, pmacoreSessionID) 90 | new_training_session_dict = {} 91 | for sess in full_training_sessions: 92 | for part, role in sess["Participants"].items(): 93 | if (part.lower() == participantUsername.lower()): 94 | s = _pma_format_training_session_properly(sess) 95 | s["Role"] = role 96 | new_training_session_dict[sess["Id"]] = s 97 | 98 | return new_training_session_dict 99 | 100 | 101 | def get_training_session_participants(pmacontrolURL, pmacontrolTrainingSessionID, pmacoreSessionID): 102 | """ 103 | Extract the participants in a particular session 104 | """ 105 | url = pma._pma_join( 106 | pmacontrolURL, 107 | "api/Sessions/" + str(pmacontrolTrainingSessionID) + "/Participants?sessionID=" + pma._pma_q(pmacoreSessionID)) 108 | try: 109 | headers = {'Accept': 'application/json'} 110 | r = pma._pma_http_get(url, headers) 111 | except Exception as e: 112 | print(e) 113 | return None 114 | parts = {} 115 | for part in r.json(): 116 | parts[part['User']] = part 117 | return parts 118 | 119 | 120 | def is_participant_in_training_session(pmacontrolURL, participantUsername, pmacontrolTrainingSessionID, 121 | pmacoreSessionID): 122 | """ 123 | Check to see if a specific user participates in a specific session 124 | """ 125 | all_parts = get_training_session_participants( 126 | pmacontrolURL, pmacontrolTrainingSessionID, pmacoreSessionID) 127 | return participantUsername in all_parts.keys() 128 | 129 | 130 | def get_training_session_url(pmacontrolURL, participantSessionID, participantUsername, pmacontrolTrainingSessionID, 131 | pmacontrolCaseCollectionID, pmacoreSessionID): 132 | if (is_participant_in_training_session(pmacontrolURL, participantUsername, pmacontrolTrainingSessionID, 133 | pmacoreSessionID)): 134 | for k, v in get_training_session(pmacontrolURL, pmacontrolTrainingSessionID, 135 | pmacoreSessionID)["CaseCollections"].items(): 136 | if k == pmacontrolCaseCollectionID: 137 | return v["Url"] + "?SessionID=" + participantSessionID 138 | else: 139 | raise ValueError("Participant " + participantUsername + 140 | " is not registered for this session") 141 | 142 | 143 | def get_all_participants(pmacontrolURL, pmacoreSessionID): 144 | """ 145 | Get a list of all participants registered across all sessions 146 | """ 147 | full_training_sessions = _pma_get_training_sessions( 148 | pmacontrolURL, pmacoreSessionID) 149 | user_dict = {} 150 | for sess in full_training_sessions: 151 | s = _pma_format_training_session_properly(sess) 152 | for part in sess["Participants"]: 153 | if not (part in user_dict): 154 | user_dict[part] = {} 155 | user_dict[part][s['Id']] = s["Title"] 156 | 157 | return user_dict 158 | 159 | 160 | def register_participant_for_training_session(pmacontrolURL, 161 | participantUsername, 162 | pmacontrolTrainingSessionID, 163 | pmacontrolRole, 164 | pmacoreSessionID, 165 | pmacontrolInteractionMode=pma_interaction_mode_locked): 166 | """ 167 | Registers a participant for a given session, assign a specific role 168 | """ 169 | # if is_participant_in_training_session(pmacontrolURL, participantUsername, pmacontrolTrainingSessionID, pmacoreSessionID): 170 | # raise NameError ("PMA.core user " + participantUsername + " is ALREADY registered in PMA.control training session " + str(pmacontrolTrainingSessionID)) 171 | url = pma._pma_join( 172 | pmacontrolURL, 173 | "api/Sessions/") + str(pmacontrolTrainingSessionID) + "/AddParticipant?SessionID=" + pmacoreSessionID 174 | data = { 175 | "UserName": participantUsername, 176 | "Role": pmacontrolRole, 177 | "InteractionMode": pmacontrolInteractionMode 178 | } # default interaction mode = Locked 179 | data = parse.urlencode(data).encode() 180 | if (pma._pma_debug is True): 181 | print("Posting to", url) 182 | print(" with payload", data) 183 | req = request.Request(url=url, data=data) # this makes the method "POST" 184 | resp = request.urlopen(req) 185 | pma._pma_clear_url_cache() 186 | return resp 187 | 188 | 189 | def register_participant_for_project(pmacontrolURL, 190 | participantUsername, 191 | pmacontrolProjectID, 192 | pmacontrolRole, 193 | pmacoreSessionID, 194 | pmacontrolInteractionMode=pma_interaction_mode_locked): 195 | """ 196 | Registers a participant for all sessions in a given project, assigning a specific role 197 | """ 198 | url = pma._pma_join(pmacontrolURL, 199 | "api/Projects/") + str(pmacontrolProjectID) + "/AddParticipant?SessionID=" + pmacoreSessionID 200 | data = { 201 | "UserName": participantUsername, 202 | "Role": pmacontrolRole, 203 | "InteractionMode": pmacontrolInteractionMode 204 | } # default interaction mode = Locked 205 | data = parse.urlencode(data).encode() 206 | if (pma._pma_debug is True): 207 | print("Posting to", url) 208 | print(" with payload", data) 209 | req = request.Request(url=url, data=data) # this makes the method "POST" 210 | resp = request.urlopen(req) 211 | pma._pma_clear_url_cache() 212 | return resp 213 | 214 | 215 | def _pma_get_case_collection_training_session_id(pmacontrolURL, pmacontrolTrainingSessionID, pmacontrolCaseCollectionID, 216 | pmacoreSessionID): 217 | full_training_sessions = _pma_get_training_sessions( 218 | pmacontrolURL, pmacoreSessionID) 219 | new_training_session_dict = {} 220 | for sess in full_training_sessions: 221 | if sess["Id"] == pmacontrolTrainingSessionID: 222 | for coll in sess["CaseCollections"]: 223 | if coll["CaseCollectionId"] == pmacontrolCaseCollectionID: 224 | return coll["Id"] 225 | return None 226 | 227 | 228 | def set_participant_interactionmode(pmacontrolURL, participantUsername, pmacontrolTrainingSessionID, 229 | pmacontrolCaseCollectionID, pmacontrolInteractionMode, pmacoreSessionID): 230 | """ 231 | Assign an interaction mode to a participant for a given Case Collection within a training session 232 | """ 233 | if not is_participant_in_training_session(pmacontrolURL, participantUsername, pmacontrolTrainingSessionID, 234 | pmacoreSessionID): 235 | raise NameError("PMA.core user " + participantUsername + 236 | " is NOT registered in PMA.control training session " + str(pmacontrolTrainingSessionID)) 237 | url = pma._pma_join( 238 | pmacontrolURL, 239 | "api/Sessions/") + str(pmacontrolTrainingSessionID) + "/InteractionMode?SessionID=" + pmacoreSessionID 240 | data = { 241 | "UserName": participantUsername, 242 | "CaseCollectionId": pmacontrolCaseCollectionID, 243 | "InteractionMode": pmacontrolInteractionMode 244 | } 245 | data = parse.urlencode(data).encode() 246 | if (pma._pma_debug is True): 247 | print("Posting to", url) 248 | print(" with payload", data) 249 | req = request.Request(url=url, data=data) # this makes the method "POST" 250 | try: 251 | resp = request.urlopen(req) 252 | except urllib.error.HTTPError as e: 253 | if (pma._pma_debug is True): 254 | print("HTTP ERROR") 255 | print(e.__dict__) 256 | return None 257 | except urllib.error.URLError as e: 258 | if (pma._pma_debug is True): 259 | print("URL ERROR") 260 | print(e.__dict__) 261 | return None 262 | pma._pma_clear_url_cache() 263 | return resp 264 | 265 | 266 | def get_training_session_titles(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID): 267 | """ 268 | Retrieve sessions (possibly filtered by project ID), titles only 269 | """ 270 | try: 271 | return list(get_training_session_titles_dict(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID).values()) 272 | except Exception as e: 273 | print(e) 274 | return None 275 | 276 | 277 | def get_training_session_titles_dict(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID): 278 | """ 279 | Retrieve (training) sessions (possibly filtered by project ID), return a dictionary of session IDs and titles 280 | """ 281 | dct = {} 282 | all = _pma_get_training_sessions(pmacontrolURL, pmacoreSessionID) 283 | for sess in all: 284 | if pmacontrolProjectID is None: 285 | dct[sess["Id"]] = sess["Title"] 286 | elif pmacontrolProjectID == sess["ProjectId"]: 287 | dct[sess["Id"]] = sess["Title"] 288 | 289 | return dct 290 | 291 | 292 | def get_training_sessions(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID): 293 | """ 294 | Retrieve (training) sessions (possibly filtered by project ID), return a dictionary of session IDs and titles 295 | """ 296 | dct = {} 297 | all = _pma_get_training_sessions(pmacontrolURL, pmacoreSessionID) 298 | for sess in all: 299 | if pmacontrolProjectID is None: 300 | dct[sess["Id"]] = _pma_format_training_session_properly(sess) 301 | elif pmacontrolProjectID == sess["ProjectId"]: 302 | dct[sess["Id"]] = _pma_format_training_session_properly(sess) 303 | 304 | return dct 305 | 306 | 307 | def get_training_session(pmacontrolURL, pmacontrolTrainingSessionID, pmacoreSessionID): 308 | """ 309 | Return the first (training) session with ID = pmacontrolTrainingSessionID 310 | """ 311 | all = _pma_get_training_sessions(pmacontrolURL, pmacoreSessionID) 312 | 313 | for el in all: 314 | if pmacontrolTrainingSessionID == el['Id']: 315 | # summarize session-related information so that it makes sense 316 | return _pma_format_training_session_properly(el) 317 | 318 | return None 319 | 320 | 321 | def search_training_session(pmacontrolURL, titleSubstring, pmacoreSessionID): 322 | """ 323 | Return the first (training) session that has titleSubstring as part of its string; search is case insensitive 324 | """ 325 | all = _pma_get_training_sessions(pmacontrolURL, pmacoreSessionID) 326 | 327 | for el in all: 328 | if titleSubstring.lower() in el['Title'].lower(): 329 | # summarize session-related information so that it makes sense 330 | return _pma_format_training_session_properly(el) 331 | 332 | return None 333 | 334 | 335 | def _pma_get_case_collections(pmacontrolURL, pmacoreSessionID): 336 | """ 337 | Retrieve all the data for all the defined case collections in PMA.control 338 | (RAW JSON data; not suited for human consumption) 339 | """ 340 | 341 | url = pma._pma_join( 342 | pmacontrolURL, "api/CaseCollections?sessionID=" + pma._pma_q(pmacoreSessionID)) 343 | try: 344 | headers = {'Accept': 'application/json'} 345 | r = pma._pma_http_get(url, headers) 346 | return r.json() 347 | except Exception as e: 348 | return None 349 | 350 | 351 | def get_case_collections(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID): 352 | """ 353 | Retrieve case collection details that belong to a specific project 354 | """ 355 | colls = {} 356 | all_colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 357 | for coll in all_colls: 358 | if (pmacontrolProjectID == coll["ProjectId"]) and not (coll["Id"] in colls.keys()): 359 | colls[coll["Id"]] = coll 360 | 361 | return colls 362 | 363 | 364 | def get_case_collection_titles(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID): 365 | """ 366 | Retrieve case collections (possibly filtered by project ID), titles only 367 | """ 368 | try: 369 | return list(get_case_collection_titles_dict(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID).values()) 370 | except Exception as e: 371 | return None 372 | 373 | 374 | def get_case_collection_titles_dict(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID): 375 | """ 376 | Retrieve case collections (possibly filtered by project ID), return a dictionary of case collection IDs and titles 377 | """ 378 | dct = {} 379 | all_colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 380 | for coll in all_colls: 381 | if pmacontrolProjectID is None: 382 | dct[coll["Id"]] = coll["Title"] 383 | elif pmacontrolProjectID == coll["ProjectId"]: 384 | dct[coll["Id"]] = coll["Title"] 385 | 386 | return dct 387 | 388 | 389 | def get_case_collection(pmacontrolURL, pmacontrolCaseCollectionID, pmacoreSessionID): 390 | """ 391 | Retrieve case collection details 392 | """ 393 | all_colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 394 | for coll in all_colls: 395 | if coll["Id"] == pmacontrolCaseCollectionID: 396 | return coll 397 | 398 | return None 399 | 400 | 401 | def get_cases_for_case_collection(pmacontrolURL, pmacontrolCaseCollectionID, pmacoreSessionID): 402 | """ 403 | Retrieve cases for a specific collection 404 | """ 405 | return get_case_collection(pmacontrolURL, pmacontrolCaseCollectionID, pmacoreSessionID)["Cases"] 406 | 407 | 408 | def search_case_collection(pmacontrolURL, titleSubstring, pmacoreSessionID): 409 | """ 410 | Return the first collection that has titleSubstring as part of its string; search is case insensitive 411 | """ 412 | all_colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 413 | for coll in all_colls: 414 | if titleSubstring.lower() in coll['Title'].lower(): 415 | # summary session-related information so that it makes sense 416 | return coll 417 | 418 | return None 419 | 420 | 421 | def _pma_format_project_embedded_training_sessions_properly(original_project_sessions): 422 | """ 423 | Helper method to convert a list of sessions with default arguments into a summarized dictionary 424 | """ 425 | dct = {} 426 | for prj_sess in original_project_sessions: 427 | dct[prj_sess["Id"]] = prj_sess["Title"] 428 | 429 | return dct 430 | 431 | 432 | def _pma_get_projects(pmacontrolURL, pmacoreSessionID): 433 | """ 434 | Retrieve all projects and their data in PMA.control 435 | (RAW JSON data; not suited for human consumption) 436 | """ 437 | url = pma._pma_join( 438 | pmacontrolURL, "api/Projects?sessionID=" + pma._pma_q(pmacoreSessionID)) 439 | try: 440 | headers = {'Accept': 'application/json'} 441 | r = pma._pma_http_get(url, headers) 442 | return r.json() 443 | except Exception as e: 444 | return None 445 | 446 | 447 | def get_projects(pmacontrolURL, pmacoreSessionID): 448 | """ 449 | Retrieve project details for all projects 450 | """ 451 | all_projects = _pma_get_projects(pmacontrolURL, pmacoreSessionID) 452 | projects = {} 453 | for prj in all_projects: 454 | # summary session-related information so that it makes sense 455 | prj['Sessions'] = _pma_format_project_embedded_training_sessions_properly( 456 | prj['Sessions']) 457 | 458 | # now integrate case collection information 459 | colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 460 | prj['CaseCollections'] = {} 461 | for col in colls: 462 | if col['ProjectId'] == prj['Id']: 463 | prj['CaseCollections'][col['Id']] = col['Title'] 464 | projects[prj['Id']] = prj 465 | 466 | return projects 467 | 468 | 469 | def get_project_titles(pmacontrolURL, pmacoreSessionID): 470 | """ 471 | Retrieve projects, return ONLY the titles 472 | """ 473 | try: 474 | return list(get_project_titles_dict(pmacontrolURL, pmacoreSessionID).values()) 475 | except Exception as e: 476 | return None 477 | 478 | 479 | def get_project_titles_dict(pmacontrolURL, pmacoreSessionID): 480 | """ 481 | Retrieve projects, return a dictionary of project-IDs and titles 482 | """ 483 | dct = {} 484 | all_projects = _pma_get_projects(pmacontrolURL, pmacoreSessionID) 485 | try: 486 | for prj in all_projects: 487 | dct[prj['Id']] = prj['Title'] 488 | except Exception as e: 489 | print(e) 490 | return None 491 | 492 | return dct 493 | 494 | 495 | def get_project(pmacontrolURL, pmacontrolProjectID, pmacoreSessionID): 496 | """ 497 | Retrieve project details 498 | """ 499 | all_projects = _pma_get_projects(pmacontrolURL, pmacoreSessionID) 500 | for prj in all_projects: 501 | if prj['Id'] == pmacontrolProjectID: 502 | # summary session-related information so that it makes sense 503 | prj['Sessions'] = _pma_format_project_embedded_training_sessions_properly( 504 | prj['Sessions']) 505 | 506 | # now integrate case collection information 507 | colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 508 | prj['CaseCollections'] = {} 509 | for col in colls: 510 | if col['ProjectId'] == prj['Id']: 511 | prj['CaseCollections'][col['Id']] = col['Title'] 512 | 513 | return prj 514 | 515 | return None 516 | 517 | 518 | def get_project_by_case_id(pmacontrolURL, pmacontrolCaseID, pmacoreSessionID): 519 | """ 520 | Retrieve case collection based on the case ID 521 | """ 522 | all_colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 523 | for coll in all_colls: 524 | for case in coll["Cases"]: 525 | if case["Id"] == pmacontrolCaseID: 526 | return get_project(pmacontrolURL, coll["ProjectId"], pmacoreSessionID) 527 | 528 | return None 529 | 530 | 531 | def get_project_by_case_collection_id(pmacontrolURL, pmacontrolCaseCollectionID, pmacoreSessionID): 532 | """ 533 | Retrieve case collection based on the case ID 534 | """ 535 | all_colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 536 | for coll in all_colls: 537 | if coll["Id"] == pmacontrolCaseCollectionID: 538 | return get_project(pmacontrolURL, coll["ProjectId"], pmacoreSessionID) 539 | 540 | return None 541 | 542 | 543 | def search_project(pmacontrolURL, titleSubstring, pmacoreSessionID): 544 | """ 545 | Return the first project that has titleSubstring as part of its string; search is case insensitive 546 | """ 547 | all_projects = _pma_get_projects(pmacontrolURL, pmacoreSessionID) 548 | 549 | for prj in all_projects: 550 | if titleSubstring.lower() in prj['Title'].lower(): 551 | # summary session-related information so that it makes sense 552 | prj['Sessions'] = _pma_format_project_embedded_training_sessions_properly( 553 | prj['Sessions']) 554 | 555 | # now integrate case collection information 556 | colls = _pma_get_case_collections(pmacontrolURL, pmacoreSessionID) 557 | prj['CaseCollections'] = {} 558 | for col in colls: 559 | if col['ProjectId'] == prj['Id']: 560 | prj['CaseCollections'][col['Id']] = col['Title'] 561 | 562 | return prj 563 | 564 | return None 565 | -------------------------------------------------------------------------------- /pma_python/core.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from pprint import pprint 3 | from PIL import Image 4 | from random import choice 5 | from io import BytesIO 6 | from urllib.parse import quote 7 | from urllib.request import urlopen 8 | from pma_python import pma 9 | 10 | # general purpose packages 11 | import os 12 | import datetime 13 | import io 14 | import shutil 15 | import re 16 | import pandas as pd 17 | 18 | import requests 19 | from requests_toolbelt.multipart.encoder import MultipartEncoder, MultipartEncoderMonitor 20 | 21 | __version__ = pma.__version__ 22 | 23 | pma_annotation_source_format_pathomation = 0 24 | pma_annotation_source_format_native = 1 25 | pma_annotation_source_format_visiopharm = 2 26 | pma_annotation_source_format_indicalabs = 3 27 | pma_annotation_source_format_aperio = 4 28 | pma_annotation_source_format_definiens = 4 29 | pma_annotation_source_format_xml = 4 30 | 31 | pma_annotation_target_format_pathomation = 2 32 | pma_annotation_target_format_visiopharm = 0 33 | pma_annotation_target_format_indicalabs = 1 34 | pma_annotation_target_format_aperio = 3 35 | pma_annotation_target_format_definiens = 3 36 | pma_annotation_target_format_xml = 3 37 | pma_annotation_target_format_csv = 4 38 | 39 | # internal module helper variables and functions 40 | _pma_sessions = dict() 41 | _pma_usernames = dict() 42 | _pma_slideinfos = dict() 43 | _pma_pmacoreliteURL = "http://localhost:54001/" 44 | _pma_pmacoreliteSessionID = "SDK.Python" 45 | _pma_usecachewhenretrievingtiles = True 46 | _pma_amount_of_data_downloaded = {_pma_pmacoreliteSessionID: 0} 47 | 48 | 49 | def set_debug_flag(flag): 50 | """ 51 | Determine whether Core module runs in debugging mode or not. 52 | When in debugging mode (flag = true), extra output is produced when certain conditions in the code are not met 53 | """ 54 | pma._pma_set_debug_flag(flag) 55 | 56 | 57 | def _pma_session_id(sessionID=None): 58 | """ 59 | Internal methods prefixed with _pma_ are not supposed to be invoked by consumers directly 60 | """ 61 | 62 | if (sessionID is None): 63 | # if the sessionID isn't specified, maybe we can still recover it somehow 64 | return _pma_first_session_id() 65 | else: 66 | # nothing to do in this case; a SessionID WAS passed along, so just continue using it 67 | return sessionID 68 | 69 | 70 | def _pma_first_session_id(): 71 | """ 72 | Internal methods prefixed with _pma_ are not supposed to be invoked by consumers directly 73 | """ 74 | 75 | # do we have any stored sessions from earlier login events? 76 | global _pma_sessions 77 | global _pma_slideinfos 78 | 79 | if (len(_pma_sessions.keys()) > 0): 80 | # yes we do! This means that when there's a PMA.core active session AND PMA.core.lite version running, 81 | # the PMA.core active will be selected and returned 82 | if pma._pma_debug == True: 83 | print("Found SessionID:", list(_pma_sessions.keys())[0]) 84 | return list(_pma_sessions.keys())[0] 85 | else: 86 | # ok, we don't have stored sessions; not a problem per se... 87 | if (_pma_is_lite()): 88 | if (_pma_pmacoreliteSessionID not in _pma_slideinfos): 89 | # _pma_sessions[_pma_pmacoreliteSessionID] = _pma_pmacoreliteURL 90 | _pma_slideinfos[_pma_pmacoreliteSessionID] = dict() 91 | if (_pma_pmacoreliteSessionID not in _pma_amount_of_data_downloaded): 92 | _pma_amount_of_data_downloaded[_pma_pmacoreliteSessionID] = 0 93 | if pma._pma_debug == True: 94 | print("Found PMA.start SessionID:", _pma_pmacoreliteSessionID) 95 | return _pma_pmacoreliteSessionID 96 | else: 97 | # no stored PMA.core sessions found NOR PMA.core.lite 98 | if pma._pma_debug == True: 99 | print("No SessionID found") 100 | return None 101 | 102 | 103 | def _pma_url(sessionID=None): 104 | """ 105 | Internal methods prefixed with _pma_ are not supposed to be invoked by consumers directly 106 | """ 107 | 108 | sessionID = _pma_session_id(sessionID) 109 | if sessionID is None: 110 | # sort of a hopeless situation; there is no URL to refer to 111 | return None 112 | elif sessionID == _pma_pmacoreliteSessionID: 113 | return _pma_pmacoreliteURL 114 | else: 115 | # assume sessionID is a valid session; otherwise the following will generate an error 116 | if sessionID in _pma_sessions.keys(): 117 | url = _pma_sessions[sessionID] 118 | if (not url.endswith("/")): 119 | url = url + "/" 120 | return url 121 | else: 122 | raise Exception("Invalid sessionID:" + str(sessionID)) 123 | 124 | 125 | def _pma_is_lite(pmacoreURL=_pma_pmacoreliteURL, verify=True): 126 | """ 127 | Internal methods prefixed with _pma_ are not supposed to be invoked by consumers directly 128 | """ 129 | 130 | # This method checks to see if PMA.core.lite (server component of PMA.start) is running at a given endpoint. 131 | # if pmacoreURL is omitted, default check is to see if PMA.start is effectively running at localhost 132 | # (defined by _pma_pmacoreliteURL). Note that PMA.start may not be running, while it is actually installed. 133 | # This method doesn't detect whether PMA.start is installed; merely whether it's running! 134 | # if pmacoreURL is specified, then the method checks if there's an instance of PMA.start (results in True), 135 | # PMA.core (results in False) or nothing (at least not a Pathomation software platform component) at all 136 | # (results in None) 137 | 138 | url = pma._pma_join(pmacoreURL, "api/json/IsLite") 139 | try: 140 | r = requests.get(url, verify=verify) 141 | print("PMA.start detected successfully") 142 | except Exception as e: 143 | # this happens when NO instance of PMA.core.lite is detected 144 | print("PMA.start not found") 145 | return None 146 | value = r.json() 147 | return value is True 148 | 149 | 150 | def _pma_api_url(sessionID=None): 151 | """ 152 | Internal methods prefixed with _pma_ are not supposed to be invoked by consumers directly 153 | """ 154 | 155 | # let's get the base URL first for the specified session 156 | url = _pma_url(sessionID) 157 | if url is None: 158 | # sort of a hopeless situation; there is no URL to refer to 159 | return None 160 | # remember, _pma_url is guaranteed to return a URL that ends with "/" 161 | return pma._pma_join(url, "api/json/") 162 | 163 | 164 | def _pma_query_url(sessionID=None): 165 | """ 166 | Internal methods prefixed with _pma_ are not supposed to be invoked by consumers directly 167 | """ 168 | 169 | # let's get the base URL first for the specified session 170 | url = _pma_url(sessionID) 171 | if url is None: 172 | # sort of a hopeless situation; there is no URL to refer to 173 | return None 174 | # remember, _pma_url is guaranteed to return a URL that ends with "/" 175 | return pma._pma_join(url, "query/json/") 176 | 177 | 178 | # end internal module helper variables and functions 179 | 180 | 181 | def is_lite(pmacoreURL=_pma_pmacoreliteURL): 182 | """ 183 | Checks to see if PMA.core.lite (server component of PMA.start) is running at a given endpoint. 184 | if pmacoreURL is omitted, default check is to see if PMA.start is effectively running at localhost 185 | (defined by _pma_pmacoreliteURL).Note that PMA.start may not be running, while it is actually installed. 186 | This method doesn't detect whether PMA.start is installed; merely whether it's running! 187 | if pmacoreURL is specified, then the method checks if there's an instance of PMA.start (results in True), 188 | PMA.core (results in False) or nothing (at least not a Pathomation software platform component) at all 189 | (results in None) 190 | """ 191 | return _pma_is_lite(pmacoreURL) 192 | 193 | 194 | def get_version_info(pmacoreURL=_pma_pmacoreliteURL, verify=True): 195 | """ 196 | Get version info from PMA.core instance running at pmacoreURL. 197 | Return None if PMA.core not found running at pmacoreURL endpoint 198 | """ 199 | # purposefully DON'T use helper function _pma_api_url() here: 200 | # why? because GetVersionInfo can be invoked WITHOUT a valid SessionID; 201 | # _pma_api_url() takes session information into account 202 | 203 | url = pma._pma_join(pmacoreURL, "api/json/GetVersionInfo") 204 | if pma._pma_debug == True: 205 | print(url) 206 | 207 | try: 208 | r = requests.get(url, verify=verify) 209 | except Exception: 210 | return None 211 | 212 | json = r.json() 213 | version = None 214 | if ("Code" in json): 215 | raise Exception("get version info resulted in: " + json["Message"]) 216 | elif ("d" in json): 217 | version = json["d"] 218 | else: 219 | version = json 220 | 221 | if version.startswith("3."): 222 | revision = get_build_revision(pmacoreURL) 223 | if revision is not None: 224 | version += "." + revision 225 | 226 | return version 227 | 228 | 229 | def get_build_revision(pmacoreURL=_pma_pmacoreliteURL, verify=True): 230 | """ 231 | Get build revision from PMA.core instance running at pmacoreURL. 232 | Return None if PMA.core not found running at pmacoreURL endpoint 233 | """ 234 | url = pma._pma_join(pmacoreURL, "api/json/GetBuildRevision") 235 | if pma._pma_debug == True: 236 | print(url) 237 | 238 | try: 239 | r = requests.get(url, verify=verify) 240 | except Exception: 241 | return None 242 | 243 | json = r.json() 244 | version = None 245 | if ("Code" in json): 246 | raise Exception("get build revision resulted in: " + json["Message"]) 247 | else: 248 | version = json 249 | 250 | return version 251 | 252 | 253 | def get_api_version(pmacoreURL=_pma_pmacoreliteURL, verify=True): 254 | """ 255 | Retrieves the API version exposed by the underlying PMA.core (no authentication or sessionID needed for this) 256 | """ 257 | 258 | url = pma._pma_join(pmacoreURL, "api/json/GetAPIVersion") 259 | if pma._pma_debug == True: 260 | print(url) 261 | 262 | try: 263 | r = requests.get(url, verify=verify) 264 | except Exception: 265 | return None 266 | 267 | try: 268 | json = r.json() 269 | except Exception: 270 | raise Exception("GetAPIVersion method not available at " + pmacoreURL) 271 | 272 | version = None 273 | if ("Code" in json): 274 | raise Exception("get_api_version resulted in: " + json["Message"]) 275 | elif ("d" in json): 276 | version = json["d"] 277 | else: 278 | version = json 279 | 280 | return version 281 | 282 | 283 | def get_api_verion_string(pmacoreURL=_pma_pmacoreliteURL): 284 | """ 285 | Returns the API version as a formatted string, rather than a list 286 | """ 287 | v = get_api_version(pmacoreURL) 288 | return ".".join([str(x) for x in v]) 289 | 290 | 291 | def register_session_id(session_id, pma_core_url): 292 | """ 293 | Registers a session ID with it's corresponding server URL 294 | """ 295 | global _pma_sessions # so afterwards we can look up what username actually belongs to a sessions 296 | global _pma_amount_of_data_downloaded 297 | global _pma_slideinfos 298 | _pma_amount_of_data_downloaded[session_id] = 0 299 | _pma_sessions[session_id] = pma_core_url 300 | _pma_slideinfos[session_id] = {} 301 | 302 | 303 | def connect(pmacoreURL=_pma_pmacoreliteURL, pmacoreUsername="", pmacorePassword="", verify=True): 304 | """ 305 | Attempt to connect to PMA.core instance; success results in a SessionID 306 | """ 307 | global _pma_sessions # so afterwards we can look up what username actually belongs to a sessions 308 | # so afterwards we can determine the PMA.core URL to connect to for a given SessionID 309 | global _pma_usernames 310 | # a caching mechanism for slide information; obsolete and should be improved 311 | global _pma_slideinfos 312 | # keep track of how much data was downloaded 313 | global _pma_amount_of_data_downloaded 314 | 315 | url = "" 316 | 317 | if (pmacoreURL == _pma_pmacoreliteURL): 318 | if is_lite(): 319 | # no point authenticating localhost / PMA.core.lite 320 | sessionID = _pma_pmacoreliteSessionID 321 | _pma_sessions[sessionID] = pmacoreURL 322 | if not (sessionID in _pma_slideinfos): 323 | _pma_slideinfos[sessionID] = {} 324 | _pma_amount_of_data_downloaded[sessionID] = 0 325 | return sessionID 326 | else: 327 | if pma._pma_debug == True: 328 | print( 329 | "PMA.start not found on (localhost); download from https://free.pathomation.com") 330 | return None 331 | 332 | headers = {'Accept': 'application/json'} 333 | # purposefully DON'T use helper function _pma_api_url() here: 334 | # why? Because_pma_api_url() takes session information into account (which we don't have yet) 335 | post_url = pma._pma_join( 336 | pmacoreURL, "api/json/authenticate?caller=SDK.Python") 337 | get_url = post_url + "&username=" + \ 338 | pma._pma_q(pmacoreUsername) + "&password=" + \ 339 | pma._pma_q(pmacorePassword) 340 | 341 | if pma._pma_debug == True: 342 | print(post_url + "&username=" + 343 | pma._pma_q(pmacoreUsername) + "&password=TOP_SECRET") 344 | 345 | try: 346 | r = requests.post(post_url, headers=headers, json={ 347 | "username": pmacoreUsername, "password": pmacorePassword, "caller": "SDK.Python"}, 348 | verify=verify) 349 | if (r.status_code != 200): 350 | raise Exception("not supported") 351 | except Exception as e: 352 | # try the get request 353 | if (pmacoreUsername != ""): 354 | url += "&username=" + pma._pma_q(pmacoreUsername) 355 | if (pma._pma_debug is True): 356 | print("Authenticating via", url + "&password=TOP_SECRET") 357 | if (pmacorePassword != ""): 358 | url += "&password=" + pma._pma_q(pmacorePassword) 359 | 360 | try: 361 | r = requests.get(url, headers=headers, verify=verify) 362 | except Exception as e: 363 | print(e) 364 | return None 365 | 366 | loginresult = r.json() 367 | 368 | if (str(loginresult["Success"]).lower() != "true"): 369 | sessionID = None 370 | else: 371 | sessionID = loginresult["SessionId"] 372 | 373 | _pma_usernames[sessionID] = pmacoreUsername 374 | _pma_sessions[sessionID] = pmacoreURL 375 | if not (sessionID in _pma_slideinfos): 376 | _pma_slideinfos[sessionID] = {} 377 | _pma_amount_of_data_downloaded[sessionID] = len(loginresult) 378 | 379 | return sessionID 380 | 381 | 382 | def disconnect(sessionID=None): 383 | """ 384 | Attempt to disconnect from a PMA.core instance 385 | """ 386 | sessionID = _pma_session_id(sessionID) 387 | url = _pma_api_url(sessionID) + \ 388 | "DeAuthenticate?sessionID=" + pma._pma_q((sessionID)) 389 | if pma._pma_debug == True: 390 | print(url) 391 | contents = urlopen(url).read() 392 | global _pma_amount_of_data_downloaded 393 | _pma_amount_of_data_downloaded[sessionID] += len(contents) 394 | if (len(_pma_sessions.keys()) > 0): 395 | # yes we do! This means that when there's a PMA.core active session AND PMA.core.lite version running, 396 | # the PMA.core active will be selected and returned 397 | del _pma_sessions[sessionID] 398 | del _pma_slideinfos[sessionID] 399 | return True 400 | 401 | 402 | def get_root_directories(sessionID=None, verify=True): 403 | """ 404 | Return an array of root-directories available to sessionID 405 | """ 406 | sessionID = _pma_session_id(sessionID) 407 | url = _pma_api_url(sessionID) + \ 408 | "GetRootDirectories?sessionID=" + pma._pma_q((sessionID)) 409 | if pma._pma_debug == True: 410 | print(url) 411 | r = requests.get(url, verify=verify) 412 | json = r.json() 413 | global _pma_amount_of_data_downloaded 414 | _pma_amount_of_data_downloaded[sessionID] += len(json) 415 | if ("Code" in json): 416 | raise Exception( 417 | "get_root_directories failed with error " + json["Message"]) 418 | return json 419 | 420 | 421 | def _pma_merge_dict_values(dicts): 422 | """ 423 | Internal methods prefixed with _pma_ are not supposed to be invoked by consumers directly 424 | """ 425 | res = [] 426 | for (_, lst) in dicts.items(): 427 | for el in lst: 428 | el = str(el) 429 | if el not in res: 430 | res.append(el) 431 | return res 432 | 433 | 434 | def analyse_corresponding_root_directories(sessionIDs): 435 | """ 436 | Return a pandas DataFrame that indicates which root-directories exist on which PMA.core instances 437 | """ 438 | # create a dictionary all_rds that contains a list of all root-directories per sessionID 439 | all_rds = {} 440 | all_urls = [] 441 | for sess in sessionIDs: 442 | url = who_am_i(sess)["url"] 443 | all_urls.append(url) 444 | rds = get_root_directories(sess) 445 | all_rds[url] = rds 446 | # pp.pprint(all_rds) 447 | 448 | # create a linear list to use as index a pandas DataFrame. 449 | # This list contains ALL root-directories, regardless of the PMA.core instance where they occur 450 | root_dirs = _pma_merge_dict_values(all_rds) 451 | 452 | # create a blank DataFrame; rows = root-directories; columns = PMA.core instances 453 | df = pd.DataFrame(index=root_dirs, columns=all_urls) 454 | 455 | # fill up the cells in the DataFrame with True or False, 456 | # depending on whether the root-dir exists at a specific instance 457 | for rd in root_dirs: 458 | for url in all_urls: 459 | for el in all_rds[url]: 460 | if str(el) == str(rd): 461 | df.loc[rd][url] = True 462 | break 463 | if not (df.loc[rd][url] is True): 464 | df.loc[rd][url] = False 465 | 466 | # Add a aggregation columns that indicates how many times a specific root-dir was found across all sessionIDs 467 | df["count"] = (df is True).sum(axis=1) 468 | return df 469 | 470 | 471 | def get_directories(startDir, sessionID=None, recursive=False, verify=True): 472 | """ 473 | Return an array of sub-directories available to sessionID in the startDir directory 474 | """ 475 | sessionID = _pma_session_id(sessionID) 476 | url = _pma_api_url(sessionID) + "GetDirectories?sessionID=" + \ 477 | pma._pma_q(sessionID) + "&path=" + pma._pma_q(startDir) 478 | if pma._pma_debug == True: 479 | print(url) 480 | r = requests.get(url, verify=verify) 481 | json = r.json() 482 | global _pma_amount_of_data_downloaded 483 | _pma_amount_of_data_downloaded[sessionID] += len(json) 484 | if ("Code" in json): 485 | raise Exception("get_directories to " + startDir + 486 | " resulted in: " + json["Message"]) 487 | elif ("d" in json): 488 | dirs = json["d"] 489 | else: 490 | dirs = json 491 | 492 | # handle recursion, if so desired 493 | if (type(recursive) == bool and recursive is True) or (type(recursive) == int and recursive > 0): 494 | for dir in get_directories(startDir, sessionID): 495 | if type(recursive) == bool: 496 | dirs = dirs + get_directories(dir, sessionID, recursive) 497 | elif type(recursive) == int: 498 | dirs = dirs + get_directories(dir, sessionID, recursive - 1) 499 | 500 | return dirs 501 | 502 | 503 | def get_first_non_empty_directory(startDir=None, sessionID=None): 504 | """ 505 | Traversing a folder hierarchy for find any non-empty data (sample slides) is a stupid repetitive task 506 | This method makes it easy to do this. 507 | When you need any sample slides on any PMA.core instance, use this method to find any folder that has some data in it 508 | """ 509 | sessionID = _pma_session_id(sessionID) 510 | 511 | if ((startDir is None) or (startDir == "")): 512 | startDir = "/" 513 | 514 | slides = None 515 | try: 516 | slides = get_slides(startDir=startDir, sessionID=sessionID) 517 | except Exception: 518 | if pma._pma_debug == True: 519 | print("Unable to examine", startDir) 520 | if (startDir != "/"): 521 | return slides 522 | 523 | if ((slides is not None) and (len(slides) > 0)): 524 | return startDir 525 | else: 526 | if (startDir == "/"): 527 | for dir in get_root_directories(sessionID=sessionID): 528 | nonEmtptyDir = get_first_non_empty_directory( 529 | startDir=dir, sessionID=sessionID) 530 | if (not (nonEmtptyDir is None)): 531 | return nonEmtptyDir 532 | else: 533 | try: 534 | dirs = get_directories(startDir, sessionID) 535 | except Exception: 536 | if pma._pma_debug == True: 537 | print("Unable to examine", startDir) 538 | else: 539 | for dir in dirs: 540 | nonEmtptyDir = get_first_non_empty_directory( 541 | startDir=dir, sessionID=sessionID) 542 | if (not (nonEmtptyDir is None)): 543 | return nonEmtptyDir 544 | return None 545 | 546 | 547 | def get_slides(startDir, sessionID=None, recursive=False, verify=True): 548 | """ 549 | Return an array of slides available to sessionID in the startDir directory 550 | The recursive argument can be either of boolean or of integer type. 551 | 552 | :param recursive : 553 | If recursive is False (boolean) or 0 (integer), no recursion takes place 554 | If recursive is True (boolean), then the folder structure will be traversed recursively down to the deepest level 555 | But setting recursive to True is actually not recommended, as you may not know how far down a folder structure goes (or just be plain wrong assuming it's shallow). 556 | A better approach therefore is to set recursive to an integer value that indicates how many levels deep the parsing should go at most. 557 | Setting recursive to 1 means that only the subfolders of startDir will be included; 558 | Setting recursive to 2 means that the subfolders AND the subfolders of these subfolders will be included. 559 | Setting recursive to 3 means that the subfolders AND the subfolders of these subfolders AND the subfolders of the subfolders of these subfolders will be included. 560 | Etcetera 561 | """ 562 | sessionID = _pma_session_id(sessionID) 563 | if (startDir.startswith("/")): 564 | startDir = startDir[1:] 565 | url = _pma_api_url(sessionID) + "GetFiles?sessionID=" + \ 566 | pma._pma_q(sessionID) + "&path=" + pma._pma_q(startDir) 567 | if pma._pma_debug == True: 568 | print(url) 569 | r = requests.get(url, verify=verify) 570 | json = r.json() 571 | global _pma_amount_of_data_downloaded 572 | _pma_amount_of_data_downloaded[sessionID] += len(json) 573 | if ("Code" in json): 574 | raise Exception("get_slides from " + startDir + 575 | " resulted in: " + json["Message"]) 576 | elif ("d" in json): 577 | slides = json["d"] 578 | else: 579 | slides = json 580 | 581 | # handle recursion, if so desired 582 | if (type(recursive) == bool and recursive is True) or (type(recursive) == int and recursive > 0): 583 | for dir in get_directories(startDir, sessionID): 584 | if type(recursive) == bool: 585 | slides = slides + get_slides(dir, sessionID, recursive) 586 | elif type(recursive) == int: 587 | slides = slides + get_slides(dir, sessionID, recursive - 1) 588 | 589 | return slides 590 | 591 | 592 | def analyse_corresponding_slides(sessionPathDict, recursive=False, includeFingerprint=False): 593 | """ 594 | Return a pandas DataFrame that indicates which slides exist on which PMA.core instances 595 | :param dict sessionPathDict: a dictionary that looks e.g. like 596 | {DevSessionID: rootDirAndPath1, ProdSessionID: rootDirAndPath2 } 597 | :param bool recursive: indicates whether the method should look in sub-directories or not 598 | """ 599 | 600 | all_slides = {} 601 | all_urls = [] 602 | for (sessionID, path) in sessionPathDict.items(): 603 | if (path[-1:]) != "/": 604 | path = path + "/" 605 | url = who_am_i(sessionID)["url"] + "[" + path + "]" 606 | all_urls.append(url) 607 | slides = get_slides(path, recursive=recursive, sessionID=sessionID) 608 | slides = [sl.replace(path, ".../") for sl in slides] 609 | all_slides[url] = slides 610 | 611 | final_slide_list = _pma_merge_dict_values(all_slides) 612 | 613 | df = pd.DataFrame(index=final_slide_list, columns=all_urls) 614 | 615 | for sl in final_slide_list: 616 | for url in all_urls: 617 | for el in all_slides[url]: 618 | if str(sl) == str(el): 619 | df.loc[sl][url] = True 620 | break 621 | if not (df.loc[sl][url] is True): 622 | df.loc[sl][url] = False 623 | 624 | df["count"] = (df is True).sum(axis=1) 625 | 626 | if includeFingerprint is True: 627 | slides_to_check = df[df["count"] == len(all_urls)] 628 | num_urls = len(all_urls) 629 | print("Number of URLs: ", num_urls) 630 | 631 | return df 632 | 633 | 634 | def get_slide_file_extension(slideRef): 635 | """ 636 | Determine the file extension for this slide 637 | """ 638 | return os.path.splitext(slideRef)[-1] 639 | 640 | 641 | def get_slide_file_name(slideRef): 642 | """ 643 | Determine the file name (with extension) for this slide 644 | """ 645 | return os.path.basename(slideRef) 646 | 647 | 648 | def get_uid(slideRef, sessionID=None, verify=True): 649 | """ 650 | Get the UID for a specific slide 651 | """ 652 | sessionID = _pma_session_id(sessionID) 653 | if (sessionID == _pma_pmacoreliteSessionID): 654 | if is_lite(): 655 | raise ValueError( 656 | "PMA.core.lite found running, but doesn't support UID generation.For advanced anonymization, please upgrade to PMA.core." 657 | ) 658 | else: 659 | raise ValueError( 660 | "PMA.core.lite not found, and besides; it doesn't support UID generation. For advanced anonymization, please upgrade to PMA.core." 661 | ) 662 | 663 | url = _pma_api_url(sessionID) + "GetUID?sessionID=" + \ 664 | pma._pma_q(sessionID) + "&path=" + pma._pma_q(slideRef) 665 | if pma._pma_debug == True: 666 | print(url) 667 | r = requests.get(url, verify=verify) 668 | json = r.json() 669 | global _pma_amount_of_data_downloaded 670 | _pma_amount_of_data_downloaded[sessionID] += len(json) 671 | if ("Code" in json): 672 | raise Exception("get_uid on " + slideRef + 673 | " resulted in: " + json["Message"]) 674 | else: 675 | uid = json 676 | return uid 677 | 678 | 679 | def get_fingerprint(slideRef, sessionID=None, verify=True): 680 | """ 681 | Get the fingerprint for a specific slide 682 | """ 683 | sessionID = _pma_session_id(sessionID) 684 | url = _pma_api_url(sessionID) + "GetFingerprint?sessionID=" + \ 685 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef) 686 | 687 | r = requests.get(url, verify=verify) 688 | json = r.json() 689 | global _pma_amount_of_data_downloaded 690 | _pma_amount_of_data_downloaded[sessionID] += len(json) 691 | if ("Code" in json): 692 | raise Exception("get_fingerprint on " + slideRef + 693 | " resulted in: " + json["Message"]) 694 | else: 695 | fingerprint = json 696 | return fingerprint 697 | 698 | 699 | def who_am_i(sessionID=None): 700 | """ 701 | Getting information about your Session 702 | """ 703 | sessionID = _pma_session_id(sessionID) 704 | retval = None 705 | 706 | if (sessionID == _pma_pmacoreliteSessionID): 707 | retval = { 708 | "sessionID": _pma_pmacoreliteSessionID, 709 | "username": None, 710 | "url": _pma_pmacoreliteURL, 711 | "amountOfDataDownloaded": _pma_amount_of_data_downloaded[_pma_pmacoreliteSessionID] 712 | } 713 | elif (sessionID is not None): 714 | retval = { 715 | "sessionID": sessionID, 716 | "username": _pma_usernames[sessionID], 717 | "url": _pma_url(sessionID), 718 | "amountOfDataDownloaded": _pma_amount_of_data_downloaded[sessionID] 719 | } 720 | 721 | return retval 722 | 723 | 724 | def sessions(): 725 | """ 726 | Return an overview of all the sessions that PMA.python currently holds in memory 727 | """ 728 | 729 | global _pma_sessions 730 | return _pma_sessions 731 | 732 | 733 | def get_tile_size(sessionID=None): 734 | """ 735 | Retrieve the standard tile size set by the PMA.core instance linked to the sessionID 736 | """ 737 | 738 | sessionID = _pma_session_id(sessionID) 739 | global _pma_slideinfos 740 | if (len(_pma_slideinfos[sessionID]) < 1): 741 | dir = get_first_non_empty_directory(sessionID) 742 | slides = get_slides(dir, sessionID) 743 | info = get_slide_info(slides[0], sessionID) 744 | else: 745 | info = choice(list(_pma_slideinfos[sessionID].values())) 746 | 747 | return (int(info["TileSize"]), int(info["TileSize"])) 748 | 749 | 750 | def get_slide_info(slideRef, sessionID=None, verify=True): 751 | """ 752 | Return raw image information in the form of nested dictionaries 753 | """ 754 | sessionID = _pma_session_id(sessionID) 755 | if (slideRef.startswith("/")): 756 | slideRef = slideRef[1:] 757 | 758 | global _pma_slideinfos 759 | 760 | if (not (slideRef in _pma_slideinfos[sessionID])): 761 | url = _pma_api_url(sessionID) + "GetImageInfo?SessionID=" + \ 762 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef) 763 | if pma._pma_debug == True: 764 | print(url) 765 | r = requests.get(url, verify=verify) 766 | if r.status_code != 200: 767 | raise Exception("ImageInfo to " + slideRef + " error") 768 | 769 | json = r.json() 770 | global _pma_amount_of_data_downloaded 771 | _pma_amount_of_data_downloaded[sessionID] += len(json) 772 | if ("Code" in json or 'Message' in json): 773 | raise Exception("ImageInfo to " + slideRef + 774 | " resulted in: " + json["Message"]) 775 | elif ("d" in json): 776 | _pma_slideinfos[sessionID][slideRef] = json["d"] 777 | else: 778 | _pma_slideinfos[sessionID][slideRef] = json 779 | elif pma._pma_debug == True: 780 | print("Getting slide info from cache") 781 | 782 | return _pma_slideinfos[sessionID][slideRef] 783 | 784 | 785 | def get_max_zoomlevel(slideRef, sessionID=None): 786 | """ 787 | Determine the maximum zoomlevel that still represents an optical magnification 788 | """ 789 | info = get_slide_info(slideRef, sessionID) 790 | if (info is None): 791 | print("Unable to get information for", slideRef, " from ", sessionID) 792 | return 0 793 | else: 794 | if ("MaxZoomLevel" in info): 795 | try: 796 | return int(info["MaxZoomLevel"]) 797 | except Exception: 798 | print("Something went wrong consulting the MaxZoomLevel key in info{} dictionary; value =", 799 | info["MaxZoomLevel"]) 800 | return 0 801 | else: 802 | try: 803 | return int(info["NumberOfZoomLevels"]) 804 | except Exception: 805 | print("Something went wrong consulting the NumberOfZoomLevels key in info{} dictionary; value =", 806 | info["NumberOfZoomLevels"]) 807 | return 0 808 | 809 | 810 | def get_zoomlevels_list(slideRef, sessionID=None, min_number_of_tiles=0): 811 | """ 812 | Obtain a list with all zoomlevels, starting with 0 and up to and including max_zoomlevel 813 | Use min_number_of_tiles argument to specify that you're only interested in zoomlevels that include at lease a given number of tiles 814 | """ 815 | return sorted(list(get_zoomlevels_dict(slideRef, sessionID, min_number_of_tiles).keys())) 816 | 817 | 818 | def get_zoomlevels_dict(slideRef, sessionID=None, min_number_of_tiles=0): 819 | """ 820 | Obtain a dictionary with the number of tiles per zoomlevel. 821 | Information is returned as (x, y, n) tuples per zoomlevel, with 822 | x = number of horizontal tiles, 823 | y = number of vertical tiles, 824 | n = total number of tiles at specified zoomlevel (x * y) 825 | Use min_number_of_tiles argument to specify that you're only interested in zoomlevels that include at lease a given number of tiles 826 | """ 827 | zoomlevels = list(range(0, get_max_zoomlevel(slideRef, sessionID) + 1)) 828 | dimensions = [ 829 | get_number_of_tiles(slideRef, z, sessionID) for z in zoomlevels 830 | if get_number_of_tiles(slideRef, z, sessionID)[2] > min_number_of_tiles 831 | ] 832 | d = dict(zip(zoomlevels[-len(dimensions):], dimensions)) 833 | 834 | return d 835 | 836 | 837 | def get_pixels_per_micrometer(slideRef, sessionID=None, zoomlevel=None,): 838 | """ 839 | Retrieve the physical dimension in terms of pixels per micrometer. 840 | When zoomlevel is left to its default value of None, dimensions at the highest zoomlevel are returned 841 | (in effect returning the "native" resolution at which the slide was registered) 842 | """ 843 | maxZoomLevel = get_max_zoomlevel(slideRef, sessionID) 844 | info = get_slide_info(slideRef, sessionID) 845 | xppm = info["MicrometresPerPixelX"] 846 | yppm = info["MicrometresPerPixelY"] 847 | if (zoomlevel is None or zoomlevel == maxZoomLevel): 848 | return (float(xppm), float(yppm)) 849 | else: 850 | factor = 2**(int(zoomlevel) - int(maxZoomLevel)) 851 | return (float(xppm) / factor, float(yppm) / factor) 852 | 853 | 854 | def get_pixel_dimensions(slideRef, sessionID=None, zoomlevel=None): 855 | """Get the total dimensions of a slide image at a given zoomlevel""" 856 | maxZoomLevel = get_max_zoomlevel(slideRef, sessionID) 857 | info = get_slide_info(slideRef, sessionID) 858 | if (zoomlevel is None or zoomlevel == maxZoomLevel): 859 | return (int(info["Width"]), int(info["Height"])) 860 | else: 861 | factor = 2**(zoomlevel - maxZoomLevel) 862 | return (int(info["Width"]) * factor, int(info["Height"]) * factor) 863 | 864 | 865 | def get_number_of_tiles(slideRef, zoomlevel=None, sessionID=None): 866 | """Determine the number of tiles needed to reconstitute a slide at a given zoomlevel""" 867 | pixels = get_pixel_dimensions(slideRef, zoomlevel, sessionID) 868 | sz = get_tile_size(sessionID) 869 | xtiles = int(ceil(pixels[0] / sz[0])) 870 | ytiles = int(ceil(pixels[1] / sz[0])) 871 | ntiles = xtiles * ytiles 872 | return (xtiles, ytiles, ntiles) 873 | 874 | 875 | def get_physical_dimensions(slideRef, sessionID=None): 876 | """Determine the physical dimensions of the sample represented by the slide. 877 | This is independent of the zoomlevel: the physical properties don't change because the magnification changes""" 878 | ppmData = get_pixels_per_micrometer(slideRef, sessionID) 879 | pixelSz = get_pixel_dimensions(slideRef, sessionID) 880 | return (pixelSz[0] * ppmData[0], pixelSz[1] * ppmData[1]) 881 | 882 | 883 | def get_number_of_channels(slideRef, sessionID=None): 884 | """Number of fluorescent channels for a slide (when slide is brightfield, return is always 1)""" 885 | info = get_slide_info(slideRef, sessionID) 886 | channels = info["TimeFrames"][0]["Layers"][0]["Channels"] 887 | return len(channels) 888 | 889 | 890 | def get_number_of_layers(slideRef, sessionID=None): 891 | """Number of (z-stacked) layers for a slide""" 892 | info = get_slide_info(slideRef, sessionID) 893 | layers = info["TimeFrames"][0]["Layers"] 894 | return len(layers) 895 | 896 | 897 | def get_number_of_z_stack_layers(slideRef, sessionID=None): 898 | """Number of z-stack layers for a slide""" 899 | return get_number_of_layers(slideRef, sessionID) 900 | 901 | 902 | def is_fluorescent(slideRef, sessionID=None): 903 | """Determine whether a slide is a fluorescent image or not""" 904 | return get_number_of_channels(slideRef, sessionID) > 1 905 | 906 | 907 | def is_multi_layer(slideRef, sessionID=None): 908 | """Determine whether a slide contains multiple (stacked) layers or not""" 909 | return get_number_of_layers(slideRef, sessionID) > 1 910 | 911 | 912 | def get_last_modified_date(slideRef, sessionID=None): 913 | info = get_slide_info(slideRef, sessionID) 914 | lms = info["LastModified"].strip("/").replace("Date(", "").replace(")", "") 915 | return datetime.datetime.fromtimestamp(int(lms) / 1000.0) 916 | 917 | 918 | def is_z_stack(slideRef, sessionID=None): 919 | """Determine whether a slide is a z-stack or not""" 920 | return is_multi_layer(slideRef, sessionID) 921 | 922 | 923 | def get_magnification(slideRef, zoomlevel=None, exact=False, sessionID=None): 924 | """Get the magnification represented at a certain zoomlevel""" 925 | ppm = get_pixels_per_micrometer(slideRef, zoomlevel, sessionID)[0] 926 | if (ppm > 0): 927 | if (exact is True): 928 | return round(40 / (ppm / 0.25)) 929 | else: 930 | return round(40 / round(ppm / 0.25)) 931 | else: 932 | return 0 933 | 934 | 935 | def get_barcode_url(slideRef, width=None, height=None, sessionID=None): 936 | """Get the URL that points to the barcode (alias for "label") for a slide""" 937 | sessionID = _pma_session_id(sessionID) 938 | if (slideRef.startswith("/")): 939 | slideRef = slideRef[1:] 940 | url = (_pma_url(sessionID) + "barcode" + "?SessionID=" + 941 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef)) 942 | if not (width is None): 943 | url = url + "&w=" + str(width) 944 | if not (height is None): 945 | url = url + "&h=" + str(height) 946 | return url 947 | 948 | 949 | def get_barcode_image(slideRef, width=None, height=None, sessionID=None, verify=True): 950 | """Get the barcode (alias for "label") image for a slide""" 951 | sessionID = _pma_session_id(sessionID) 952 | if (slideRef.startswith("/")): 953 | slideRef = slideRef[1:] 954 | url = get_barcode_url(slideRef, width, height, sessionID) 955 | r = requests.get(url, verify=verify) 956 | if pma._pma_debug == True: 957 | print(url) 958 | img = Image.open(BytesIO(r.content)) 959 | global _pma_amount_of_data_downloaded 960 | _pma_amount_of_data_downloaded[sessionID] += len(r.content) 961 | return img 962 | 963 | 964 | def get_barcode_text(slideRef, sessionID=None, verify=True): 965 | """Get the text encoded by the barcode (if there IS a barcode on the slide to begin with)""" 966 | sessionID = _pma_session_id(sessionID) 967 | if (slideRef.startswith("/")): 968 | slideRef = slideRef[1:] 969 | url = _pma_api_url(sessionID) + "GetBarcodeText?sessionID=" + \ 970 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef) 971 | if pma._pma_debug == True: 972 | print(url) 973 | r = requests.get(url, verify=verify) 974 | if ((not (r.text is None)) and (len(r.text) > 0)): 975 | json = r.json() 976 | global _pma_amount_of_data_downloaded 977 | _pma_amount_of_data_downloaded[sessionID] += len(json) 978 | if ("Code" in json): 979 | raise Exception("get_barcode_text on " + slideRef + 980 | " resulted in: " + json["Message"]) 981 | else: 982 | barcode = json 983 | else: 984 | barcode = "" 985 | return barcode 986 | 987 | 988 | def get_label_url(slideRef, width=None, height=None, sessionID=None): 989 | """Get the URL that points to the label for a slide""" 990 | return get_barcode_url(slideRef, width, height, sessionID) 991 | 992 | 993 | def get_label_image(slideRef, width=None, height=None, sessionID=None): 994 | """Get the label image for a slide""" 995 | return get_barcode_image(slideRef, width, height, sessionID) 996 | 997 | 998 | def get_thumbnail_url(slideRef, width=None, height=None, sessionID=None): 999 | """Get the URL that points to the thumbnail for a slide""" 1000 | sessionID = _pma_session_id(sessionID) 1001 | if (slideRef.startswith("/")): 1002 | slideRef = slideRef[1:] 1003 | url = (_pma_url(sessionID) + "thumbnail" + "?SessionID=" + 1004 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef)) 1005 | if not (width is None): 1006 | url = url + "&w=" + str(width) 1007 | if not (height is None): 1008 | url = url + "&h=" + str(height) 1009 | return url 1010 | 1011 | 1012 | def get_thumbnail_image(slideRef, width=None, height=None, sessionID=None, verify=True): 1013 | """Get the thumbnail image for a slide""" 1014 | sessionID = _pma_session_id(sessionID) 1015 | if (slideRef.startswith("/")): 1016 | slideRef = slideRef[1:] 1017 | url = get_thumbnail_url(slideRef, width, height, sessionID) 1018 | if pma._pma_debug == True: 1019 | print(url) 1020 | r = requests.get(url, verify=verify) 1021 | img = Image.open(BytesIO(r.content)) 1022 | global _pma_amount_of_data_downloaded 1023 | _pma_amount_of_data_downloaded[sessionID] += len(r.content) 1024 | return img 1025 | 1026 | 1027 | def get_macro_url(slideRef, width=None, height=None, sessionID=None): 1028 | """Get the URL that points to the macro image (thumbnail + label) for a slide""" 1029 | sessionID = _pma_session_id(sessionID) 1030 | if (slideRef.startswith("/")): 1031 | slideRef = slideRef[1:] 1032 | url = (_pma_url(sessionID) + "macro" + "?SessionID=" + 1033 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef)) 1034 | if not (width is None): 1035 | url = url + "&w=" + str(width) 1036 | if not (height is None): 1037 | url = url + "&h=" + str(height) 1038 | return url 1039 | 1040 | 1041 | def get_macro_image(slideRef, width=None, height=None, sessionID=None, verify=True): 1042 | """Get the macro image for a slide""" 1043 | sessionID = _pma_session_id(sessionID) 1044 | if (slideRef.startswith("/")): 1045 | slideRef = slideRef[1:] 1046 | url = get_macro_url(slideRef, width, height, sessionID) 1047 | if pma._pma_debug == True: 1048 | print(url) 1049 | r = requests.get(url, verify=verify) 1050 | img = Image.open(BytesIO(r.content)) 1051 | global _pma_amount_of_data_downloaded 1052 | _pma_amount_of_data_downloaded[sessionID] += len(r.content) 1053 | return img 1054 | 1055 | 1056 | def get_tile_url(slideRef, x=0, y=0, zoomlevel=None, zstack=0, sessionID=None, format="jpg", quality=100, verify=True): 1057 | """ 1058 | Get a single tile at position (x, y) 1059 | Format can be 'jpg' or 'png' 1060 | Quality is an integer value and varies from 0 (as much compression as possible; not recommended) to 100 (100%, no compression) 1061 | """ 1062 | sessionID = _pma_session_id(sessionID) 1063 | if (slideRef.startswith("/")): 1064 | slideRef = slideRef[1:] 1065 | if (zoomlevel is None): 1066 | zoomlevel = 0 # get_max_zoomlevel(slideRef, sessionID) 1067 | 1068 | url = _pma_url(sessionID) + "tile" 1069 | if url is None: 1070 | raise Exception( 1071 | "Unable to determine the PMA.core instance belonging to " + str(sessionID)) 1072 | 1073 | params = { 1074 | "sessionID": sessionID, 1075 | "channels": 0, 1076 | "timeframe": 0, 1077 | "layer": int(round(zstack)), 1078 | "pathOrUid": slideRef, 1079 | "x": int(round(x)), 1080 | "y": int(round(y)), 1081 | "z": int(round(zoomlevel)), 1082 | "format": format, 1083 | "quality": quality, 1084 | "cache": str(_pma_usecachewhenretrievingtiles).lower() 1085 | } 1086 | 1087 | r = requests.get(url, params=params, verify=verify) 1088 | return r.request.url 1089 | 1090 | 1091 | def get_tile(slideRef, x=0, y=0, zoomlevel=None, zstack=0, sessionID=None, format="jpg", quality=100, verify=True): 1092 | """ 1093 | Get a single tile at position (x, y) 1094 | Format can be 'jpg' or 'png' 1095 | Quality is an integer value and varies from 0 (as much compression as possible; not recommended) to 100 (100%, no compression) 1096 | """ 1097 | sessionID = _pma_session_id(sessionID) 1098 | if (slideRef.startswith("/")): 1099 | slideRef = slideRef[1:] 1100 | if (zoomlevel is None): 1101 | zoomlevel = 0 # get_max_zoomlevel(slideRef, sessionID) 1102 | 1103 | url = _pma_url(sessionID) + "tile" 1104 | if url is None: 1105 | raise Exception( 1106 | "Unable to determine the PMA.core instance belonging to " + str(sessionID)) 1107 | 1108 | params = { 1109 | "sessionID": sessionID, 1110 | "channels": 0, 1111 | "timeframe": 0, 1112 | "layer": int(round(zstack)), 1113 | "pathOrUid": slideRef, 1114 | "x": int(round(x)), 1115 | "y": int(round(y)), 1116 | "z": int(round(zoomlevel)), 1117 | "format": format, 1118 | "quality": quality, 1119 | "cache": str(_pma_usecachewhenretrievingtiles).lower() 1120 | } 1121 | 1122 | if pma._pma_debug == True: 1123 | print(url) 1124 | 1125 | r = requests.get(url, params=params, verify=verify) 1126 | img = Image.open(BytesIO(r.content)) 1127 | global _pma_amount_of_data_downloaded 1128 | _pma_amount_of_data_downloaded[sessionID] += len(r.content) 1129 | return img 1130 | 1131 | 1132 | def get_region(slideRef, x=0, y=0, width=0, height=0, scale=1, zstack=0, sessionID=None, format="jpg", quality=100, rotation=0, 1133 | contrast=None, brightness=None, postGamma=None, dpi=300, flipVertical=False, flipHorizontal=False, annotationsLayerType=None, drawFilename=0, 1134 | downloadInsteadOfDisplay=False, drawScaleBar=False, gamma=[], channelClipping=[], verify=True): 1135 | """ 1136 | Gets a region of the slide at the specified scale 1137 | Format can be 'jpg' or 'png' 1138 | Quality is an integer value and varies from 0 (as much compression as possible; not recommended) to 100 (100%, no compression) 1139 | x,y,width,height is the region to get 1140 | rotation is the rotation in degrees of the slide to get 1141 | """ 1142 | sessionID = _pma_session_id(sessionID) 1143 | if (slideRef.startswith("/")): 1144 | slideRef = slideRef[1:] 1145 | 1146 | url = _pma_url(sessionID) + "region" 1147 | if url is None: 1148 | raise Exception( 1149 | "Unable to determine the PMA.core instance belonging to " + str(sessionID)) 1150 | 1151 | params = { 1152 | "sessionID": sessionID, 1153 | "channels": 0, 1154 | "timeframe": 0, 1155 | "layer": int(round(zstack)), 1156 | "pathOrUid": slideRef, 1157 | "x": int(round(x)), 1158 | "y": int(round(y)), 1159 | "width": int(round(width)), 1160 | "height": int(round(height)), 1161 | "scale": float(scale), 1162 | "format": format, 1163 | "quality": quality, 1164 | "rotation": float(rotation), 1165 | "contrast": contrast, 1166 | "brightness": brightness, 1167 | "postGamma": postGamma, 1168 | "dpi": dpi, 1169 | "flipVertical": flipVertical, 1170 | "flipHorizontal": flipHorizontal, 1171 | "annotationsLayerType": annotationsLayerType, 1172 | "drawFilename": drawFilename, 1173 | "downloadInsteadOfDisplay": downloadInsteadOfDisplay, 1174 | "drawScaleBar": drawScaleBar, 1175 | "gamma": ",".join([str(s) for s in gamma]), 1176 | "channelClipping": ",".join([str(s) for s in channelClipping]) 1177 | } 1178 | 1179 | if pma._pma_debug == True: 1180 | print(url) 1181 | 1182 | r = requests.get(url, params=params, verify=verify) 1183 | img = Image.open(BytesIO(r.content)) 1184 | global _pma_amount_of_data_downloaded 1185 | _pma_amount_of_data_downloaded[sessionID] += len(r.content) 1186 | return img 1187 | 1188 | 1189 | def get_submitted_forms(slideRef, sessionID=None, verify=True): 1190 | """Find out what forms where submitted for a specific slide""" 1191 | sessionID = _pma_session_id(sessionID) 1192 | if (slideRef.startswith("/")): 1193 | slideRef = slideRef[1:] 1194 | url = _pma_api_url(sessionID) + "GetFormSubmissions?sessionID=" + \ 1195 | pma._pma_q(sessionID) + "&pathOrUids=" + pma._pma_q(slideRef) 1196 | all_forms = get_available_forms(slideRef, sessionID) 1197 | if pma._pma_debug == True: 1198 | print(url) 1199 | r = requests.get(url, verify=verify) 1200 | if ((not (r.text is None)) and (len(r.text) > 0)): 1201 | json = r.json() 1202 | global _pma_amount_of_data_downloaded 1203 | _pma_amount_of_data_downloaded[sessionID] += len(json) 1204 | if ("Code" in json): 1205 | raise Exception("get_available_forms on " + 1206 | slideRef + " resulted in: " + json["Message"]) 1207 | else: 1208 | data = json 1209 | forms = {} 1210 | for entry in data: 1211 | if (not (entry["FormID"] in forms)): 1212 | forms[entry["FormID"]] = all_forms[entry["FormID"]] 1213 | # should probably do some post-processing here, but unsure what that would actually be?? 1214 | else: 1215 | forms = "" 1216 | return forms 1217 | 1218 | 1219 | def get_submitted_form_data(slideRef, sessionID=None, verify=True): 1220 | """Get all submitted form data associated with a specific slide""" 1221 | sessionID = _pma_session_id(sessionID) 1222 | if (slideRef.startswith("/")): 1223 | slideRef = slideRef[1:] 1224 | url = _pma_api_url(sessionID) + "GetFormSubmissions?sessionID=" + \ 1225 | pma._pma_q(sessionID) + "&pathOrUids=" + pma._pma_q(slideRef) 1226 | if pma._pma_debug == True: 1227 | print(url) 1228 | r = requests.get(url, verify=verify) 1229 | if ((not (r.text is None)) and (len(r.text) > 0)): 1230 | json = r.json() 1231 | global _pma_amount_of_data_downloaded 1232 | _pma_amount_of_data_downloaded[sessionID] += len(json) 1233 | if ("Code" in json): 1234 | raise Exception("get_available_forms on " + 1235 | slideRef + " resulted in: " + json["Message"]) 1236 | else: 1237 | data = json 1238 | # should probably do some post-processing here, but unsure what that would actually be?? 1239 | else: 1240 | data = "" 1241 | return data 1242 | 1243 | 1244 | def get_available_forms(slideRef=None, sessionID=None, verify=True): 1245 | """ 1246 | See what forms are available to fill out, either system-wide (leave slideref to None), or for a particular slide 1247 | """ 1248 | sessionID = _pma_session_id(sessionID) 1249 | if (slideRef is not None): 1250 | if (slideRef.startswith("/")): 1251 | slideRef = slideRef[1:] 1252 | dir = os.path.split(slideRef)[0] 1253 | url = _pma_api_url(sessionID) + "GetForms?sessionID=" + \ 1254 | pma._pma_q(sessionID) + "&path=" + pma._pma_q(dir) 1255 | else: 1256 | url = _pma_api_url(sessionID) + \ 1257 | "GetForms?sessionID=" + pma._pma_q(sessionID) 1258 | 1259 | if pma._pma_debug == True: 1260 | print(url) 1261 | 1262 | r = requests.get(url, verify=verify) 1263 | if ((not (r.text is None)) and (len(r.text) > 0)): 1264 | json = r.json() 1265 | global _pma_amount_of_data_downloaded 1266 | _pma_amount_of_data_downloaded[sessionID] += len(json) 1267 | if ("Code" in json): 1268 | raise Exception("get_available_forms on " + 1269 | slideRef + " resulted in: " + json["Message"]) 1270 | else: 1271 | forms_json = json 1272 | forms = {} 1273 | for entry in forms_json: 1274 | forms[entry["Key"]] = entry["Value"] 1275 | else: 1276 | forms = "" 1277 | return forms 1278 | 1279 | 1280 | def prepare_form_dictionary(formID, sessionID=None, verify=True): 1281 | """Prepare a form-dictionary that can be used later on to submit new form data for a slide""" 1282 | if (formID is None): 1283 | return None 1284 | sessionID = _pma_session_id(sessionID) 1285 | url = _pma_api_url(sessionID) + \ 1286 | "GetFormDefinitions?sessionID=" + pma._pma_q(sessionID) 1287 | if pma._pma_debug == True: 1288 | print(url) 1289 | r = requests.get(url, verify=verify) 1290 | if ((not (r.text is None)) and (len(r.text) > 0)): 1291 | json = r.json() 1292 | global _pma_amount_of_data_downloaded 1293 | _pma_amount_of_data_downloaded[sessionID] += len(json) 1294 | if ("Code" in json): 1295 | raise Exception("get_available_forms on " + 1296 | formID + " resulted in: " + json["Message"]) 1297 | else: 1298 | forms_json = json 1299 | form_def = {} 1300 | for form in forms_json: 1301 | if ((form["FormID"] == formID) or (form["FormName"] == formID)): 1302 | for field in form["FormFields"]: 1303 | form_def[field["Label"]] = None 1304 | else: 1305 | form_def = "" 1306 | return form_def 1307 | 1308 | 1309 | def submit_form_data(slideRef, formID, formDict, sessionID=None): 1310 | """Not implemented yet""" 1311 | sessionID = _pma_session_id(sessionID) 1312 | if (slideRef is not None): 1313 | if (slideRef.startswith("/")): 1314 | slideRef = slideRef[1:] 1315 | return None 1316 | 1317 | 1318 | def get_annotations(slideRef, sessionID=None, verify=True): 1319 | """ 1320 | Retrieve the annotations for slide slideRef 1321 | """ 1322 | sessionID = _pma_session_id(sessionID) 1323 | if (slideRef.startswith("/")): 1324 | slideRef = slideRef[1:] 1325 | dir = os.path.split(slideRef)[0] 1326 | url = _pma_api_url(sessionID) + "GetAnnotations?sessionID=" + \ 1327 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef) 1328 | if pma._pma_debug == True: 1329 | print(url) 1330 | 1331 | r = requests.get(url, verify=verify) 1332 | if ((not (r.text is None)) and (len(r.text) > 0)): 1333 | json = r.json() 1334 | global _pma_amount_of_data_downloaded 1335 | _pma_amount_of_data_downloaded[sessionID] += len(json) 1336 | if ("Code" in json): 1337 | raise Exception("get_annotations() on " + 1338 | slideRef + " resulted in: " + json["Message"]) 1339 | else: 1340 | annotations = json 1341 | else: 1342 | annotations = "" 1343 | return annotations 1344 | 1345 | 1346 | def export_annotations(slideRef, annotation_source_format=[pma_annotation_source_format_pathomation], annotation_target_format=pma_annotation_target_format_xml, sessionID=None, verify=True): 1347 | """ 1348 | Retrieve the annotations for slide slideRef 1349 | """ 1350 | sessionID = _pma_session_id(sessionID) 1351 | if (not (isinstance(annotation_source_format, list))): 1352 | annotation_source_format = [str(annotation_source_format)] 1353 | if (slideRef.startswith("/")): 1354 | slideRef = slideRef[1:] 1355 | source_format = "&source=" + pma._pma_q(",".join(annotation_source_format)) 1356 | tgt_format = "&format=" + pma._pma_q(str(annotation_target_format)) 1357 | url = _pma_api_url(sessionID) + "ExportAnnotations?sessionID=" + pma._pma_q( 1358 | sessionID) + "&pathOrUid=" + pma._pma_q(slideRef) + tgt_format + source_format 1359 | if (pma._pma_debug is True): 1360 | print(url) 1361 | 1362 | r = requests.get(url, verify=verify) 1363 | 1364 | if (r.ok): 1365 | return BytesIO(r.content) 1366 | else: 1367 | raise ValueError("Unable to get annotations (" + str(r.status_code) + ")") 1368 | return None 1369 | 1370 | 1371 | def get_tiles(slideRef, 1372 | fromX=0, 1373 | fromY=0, 1374 | toX=None, 1375 | toY=None, 1376 | zoomlevel=None, 1377 | zstack=0, 1378 | sessionID=None, 1379 | format="jpg", 1380 | quality=100): 1381 | """ 1382 | Get all tiles with a (fromX, fromY, toX, toY) rectangle. Navigate left to right, top to bottom 1383 | Format can be 'jpg' or 'png' 1384 | Quality is an integer value and varies from 0 (as much compression as possible; not recommended) to 100 (100%, no compression) 1385 | """ 1386 | sessionID = _pma_session_id(sessionID) 1387 | if (slideRef.startswith("/")): 1388 | slideRef = slideRef[1:] 1389 | 1390 | if (zoomlevel is None): 1391 | zoomlevel = 0 # get_max_zoomlevel(slideRef, sessionID) 1392 | if (toX is None): 1393 | toX = get_number_of_tiles(slideRef, zoomlevel, sessionID)[0] 1394 | if (toY is None): 1395 | toY = get_number_of_tiles(slideRef, zoomlevel, sessionID)[1] 1396 | for x in range(fromX, toX): 1397 | for y in range(fromY, toY): 1398 | yield get_tile(slideRef=slideRef, 1399 | x=x, 1400 | y=y, 1401 | zstack=zstack, 1402 | zoomlevel=zoomlevel, 1403 | sessionID=sessionID, 1404 | format=format, 1405 | quality=quality) 1406 | 1407 | 1408 | def show_slide(slideRef, sessionID=None): 1409 | """Launch the default webbrowser and load a web-based viewer for the slide""" 1410 | sessionID = _pma_session_id(sessionID) 1411 | if (slideRef.startswith("/")): 1412 | slideRef = slideRef[1:] 1413 | if (os.name == "posix"): 1414 | os_cmd = "open " 1415 | else: 1416 | os_cmd = "start " 1417 | 1418 | if (sessionID == _pma_pmacoreliteSessionID): 1419 | url = "http://localhost:54001/app?path=" + \ 1420 | pma._pma_q(slideRef) 1421 | else: 1422 | url = _pma_url(sessionID) 1423 | if url is None: 1424 | raise Exception( 1425 | "Unable to determine the PMA.core instance belonging to " + str(sessionID)) 1426 | else: 1427 | poUid = "&pathOrUid=" if os.name == "posix" else "^&pathOrUid=" 1428 | url += "viewer/index.htm" + "?sessionID=" + \ 1429 | pma._pma_q(sessionID) + poUid + pma._pma_q(slideRef) 1430 | 1431 | if (pma._pma_debug == True): 1432 | print(url) 1433 | if (os.name == "posix"): 1434 | url = f"\"{url}\"" 1435 | os.system(os_cmd + url) 1436 | 1437 | 1438 | def get_files_for_slide(slideRef, sessionID=None, verify=True): 1439 | """Obtain all files actually associated with a specific slide 1440 | This is most relevant with slides that are defined by multiple files, like MRXS or VSI""" 1441 | sessionID = _pma_session_id(sessionID) 1442 | 1443 | if (slideRef.startswith("/")): 1444 | slideRef = slideRef[1:] 1445 | 1446 | if (sessionID == _pma_pmacoreliteSessionID): 1447 | url = _pma_api_url(sessionID) + "EnumerateAllFilesForSlide?sessionID=" + pma._pma_q( 1448 | sessionID) + "&pathOrUid=" + pma._pma_q(slideRef) 1449 | else: 1450 | url = _pma_api_url(sessionID) + "getfilenames?sessionID=" + \ 1451 | pma._pma_q(sessionID) + "&pathOrUid=" + pma._pma_q(slideRef) 1452 | 1453 | if pma._pma_debug == True: 1454 | print(url) 1455 | 1456 | r = requests.get(url, verify=verify) 1457 | json = r.json() 1458 | global _pma_amount_of_data_downloaded 1459 | _pma_amount_of_data_downloaded[sessionID] += len(json) 1460 | if ("Code" in json): 1461 | raise Exception("enumerate_files_for_slide on " + 1462 | slideRef + " resulted in: " + json["Message"]) 1463 | elif ("d" in json): 1464 | files = json["d"] 1465 | else: 1466 | files = json 1467 | 1468 | retval = {} 1469 | for file in files: 1470 | if (sessionID == _pma_pmacoreliteSessionID): 1471 | retval[file] = {"Size": 0, "LastModified": None} 1472 | else: 1473 | retval[file["Path"]] = {"Size": file["Size"], 1474 | "LastModified": file["LastModified"]} 1475 | 1476 | return retval 1477 | 1478 | 1479 | def search_slides(startDir, pattern, sessionID=None, verify=True): 1480 | sessionID = _pma_session_id(sessionID) 1481 | if (sessionID == _pma_pmacoreliteSessionID): 1482 | if is_lite(): 1483 | raise ValueError( 1484 | "PMA.core.lite found running, but doesn't support searching.") 1485 | else: 1486 | raise ValueError( 1487 | "PMA.core.lite not found, and besides; it doesn't support searching.") 1488 | 1489 | if (startDir.startswith("/")): 1490 | startDir = startDir[1:] 1491 | 1492 | url = _pma_query_url(sessionID) + "Filename?sessionID=" + pma._pma_q(sessionID) + "&path=" + pma._pma_q( 1493 | startDir) + "&pattern=" + pma._pma_q(pattern) 1494 | if pma._pma_debug == True: 1495 | print("url =", url) 1496 | 1497 | r = requests.get(url, verify=verify) 1498 | json = r.json() 1499 | global _pma_amount_of_data_downloaded 1500 | _pma_amount_of_data_downloaded[sessionID] += len(json) 1501 | if ("Code" in json): 1502 | raise Exception("search_slides on " + startDir + 1503 | " resulted in: " + json["Message"]) 1504 | elif ("d" in json): 1505 | files = json["d"] 1506 | else: 1507 | files = json 1508 | return files 1509 | 1510 | 1511 | def _pma_upload_callback(monitor, filename): 1512 | v = monitor.bytes_read / monitor.len 1513 | if not monitor.previous or v - monitor.previous > 0.05 or (v - monitor.previous > 0 and monitor.bytes_read == monitor.len): 1514 | print("{0:.0%}".format(monitor.bytes_read / monitor.len)) 1515 | monitor.previous = v 1516 | 1517 | 1518 | def _pma_upload_amazon_callback(bytes_read, total_size, previous, filename): 1519 | v = min(1, bytes_read / total_size) 1520 | if not previous or v - previous > 0.05 or (v - previous > 0 and bytes_read == total_size): 1521 | print("{0:.0%}".format(v)) 1522 | return v 1523 | 1524 | 1525 | def upload(local_source_slide, target_folder, target_pma_core_sessionID, callback=None, verify=True): 1526 | """ 1527 | Uploads a slide to a PMA.core server. Requires a PMA.start installation 1528 | :param str local_source_slide: The local PMA.start relative file to upload 1529 | :param str target_folder: The root directory and path to upload to the PMA.core server 1530 | :param str target_pma_core_sessionID: A valid session id for a PMA.core server 1531 | :param function|boolean callback: If True a default progress will be printed. 1532 | If a function is passed it will be called for progress on each file upload. 1533 | The function has the following signature: 1534 | `callback(bytes_read, bytes_length, filename)` 1535 | """ 1536 | if not _pma_is_lite(): 1537 | raise Exception( 1538 | "No PMA.start found on localhost. Are you sure it is running?") 1539 | 1540 | if not target_folder: 1541 | raise ValueError("target_folder cannot be empty") 1542 | 1543 | if (target_folder.startswith("/")): 1544 | target_folder = target_folder[1:] 1545 | 1546 | files = get_files_for_slide(local_source_slide, _pma_pmacoreliteSessionID) 1547 | sessionID = _pma_session_id(target_pma_core_sessionID) 1548 | url = _pma_url(sessionID) + "transfer/Upload?sessionID=" + \ 1549 | pma._pma_q(sessionID) 1550 | 1551 | mainDirectory = '' 1552 | for i, f in enumerate(files): 1553 | md = os.path.dirname(f) 1554 | if i == 0 or len(md) < len(mainDirectory): 1555 | mainDirectory = md 1556 | 1557 | uploadFiles = [] 1558 | for i, filepath in enumerate(files): 1559 | s = os.path.getsize(filepath) 1560 | if s > 0: 1561 | uploadFiles.append({ 1562 | "Path": filepath.replace(mainDirectory, '').strip("\\").strip('/'), 1563 | "Length": s, 1564 | "IsMain": i == len(files) - 1, 1565 | "FullPath": filepath 1566 | }) 1567 | 1568 | data = {"Path": target_folder, "Files": uploadFiles} 1569 | 1570 | uploadHeaderResponse = requests.post(url, json=data, verify=verify) 1571 | if not uploadHeaderResponse.status_code == 200: 1572 | print(uploadHeaderResponse.json()) 1573 | raise Exception(uploadHeaderResponse.json()["Message"]) 1574 | 1575 | uploadHeader = uploadHeaderResponse.json() 1576 | 1577 | pmaCoreUploadUrl = _pma_url(sessionID) + "transfer/Upload/" + pma._pma_q( 1578 | uploadHeader["Id"]) + "?sessionID=" + pma._pma_q(sessionID) + "&path={0}" 1579 | 1580 | isAmazonUpload = True 1581 | if not uploadHeader['Urls']: 1582 | isAmazonUpload = False 1583 | uploadHeader['Urls'] = [pmaCoreUploadUrl.format( 1584 | f["Path"]) for f in uploadFiles] 1585 | 1586 | for i, f in enumerate(uploadFiles): 1587 | uploadUrl = uploadHeader['Urls'][i] 1588 | 1589 | e = MultipartEncoder( 1590 | fields={"file": (os.path.basename(f["Path"]), open(f["FullPath"], 'rb'), 'application/octet-stream')}) 1591 | 1592 | _callback = None 1593 | if callback is True: 1594 | print("Uploading file: {0}".format(e.fields["file"][0])) 1595 | if not isAmazonUpload: 1596 | def _callback(x): return _pma_upload_callback( 1597 | monitor, e.fields["file"][0]) 1598 | else: 1599 | def _callback(bytes_read, total_size, previous): return _pma_upload_amazon_callback( 1600 | bytes_read, total_size, previous, e.fields["file"][0]) 1601 | elif callable(callback): 1602 | def _callback(x): return callback( 1603 | x.bytes_read, x.len, x.previous, e.fields["file"][0]) 1604 | 1605 | monitor = MultipartEncoderMonitor(e, _callback) 1606 | 1607 | monitor.previous = 0 1608 | 1609 | r = None 1610 | if not isAmazonUpload: 1611 | r = requests.post(uploadUrl, data=monitor, headers={ 1612 | 'Content-Type': monitor.content_type}, 1613 | verify=verify) 1614 | else: 1615 | headers = {'Content-Length': str(f["Length"])} 1616 | if uploadHeader['UploadType'] == 2: 1617 | headers = { 1618 | 'Content-Length': str(f["Length"]), 'x-ms-blob-type': 'BlockBlob'} 1619 | 1620 | r = requests.put(uploadUrl, data=UploadChunksIterator( 1621 | open(f["FullPath"], 'rb'), f["Path"], f["Length"], _callback), headers=headers, verify=verify) 1622 | 1623 | if r.status_code < 200 or r.status_code >= 300: 1624 | raise Exception("Error uploading file {0}: {1} \r\n{2}: {3}".format( 1625 | f["Path"], uploadUrl, r.status_code, r.text)) 1626 | 1627 | uploadFinalizeResponse = requests.get(_pma_url(sessionID) + "transfer/Upload/" 1628 | + pma._pma_q(uploadHeader["Id"]) + "?sessionID=" + pma._pma_q(sessionID), verify=verify) 1629 | if uploadFinalizeResponse.status_code < 200 or uploadFinalizeResponse.status_code >= 300: 1630 | print(uploadFinalizeResponse.json()) 1631 | raise Exception(uploadFinalizeResponse.json()[ 1632 | "Message"] + uploadFinalizeResponse.json()["ExceptionMessage"]) 1633 | 1634 | 1635 | class UploadChunksIterator: 1636 | def __init__(self, file: io.BufferedReader, filename, total_size: int, callback, chunk_size: int = 16 * 1024): 1637 | self.file = file 1638 | self.filename = filename 1639 | self.chunk_size = chunk_size 1640 | self.total_size = total_size 1641 | self.callback = callback 1642 | self.bytes_read = 0 1643 | self.previous = 0 1644 | 1645 | def __iter__(self): 1646 | return self 1647 | 1648 | def __next__(self): 1649 | data = self.file.read(self.chunk_size) 1650 | 1651 | if not data: 1652 | raise StopIteration 1653 | self.bytes_read += self.chunk_size 1654 | if self.callback is not None: 1655 | v = self.callback(self.bytes_read, self.total_size, self.previous) 1656 | if v is not None: 1657 | self.previous = v 1658 | return data 1659 | 1660 | def __len__(self): 1661 | return self.total_size 1662 | 1663 | 1664 | def download(slideRef, save_directory=None, sessionID=None, verify=True): 1665 | """ 1666 | Downloads a slide from a PMA.core server. 1667 | :param str slideRef: The virtual path to the slide 1668 | :param str save_directory: The local directory to save the downloaded files to 1669 | :param str sessionID: The sessionID to authenticate to the pma.core server 1670 | """ 1671 | def get_filename_from_cd(cd): 1672 | """ 1673 | Get filename from content-disposition 1674 | """ 1675 | if not cd: 1676 | return None 1677 | fname = re.findall('filename=(\S+)', cd) 1678 | if len(fname) == 0: 1679 | return None 1680 | return fname[0].strip(";") 1681 | 1682 | if not slideRef: 1683 | raise ValueError("slide cannot be empty") 1684 | 1685 | if save_directory and not os.path.exists(save_directory): 1686 | raise ValueError( 1687 | "The output directory does not exist {}".format(save_directory)) 1688 | 1689 | sessionID = _pma_session_id(sessionID) 1690 | files = get_files_for_slide(slideRef, sessionID) 1691 | if not files: 1692 | raise ValueError("Slide not found") 1693 | 1694 | mainDirectory = slideRef.rsplit('/', 1)[0] 1695 | 1696 | for f in files: 1697 | relativePath = f.replace(mainDirectory, '').strip("\\").strip("/") 1698 | pmaCoreDownloadUrl = _pma_url(sessionID) + "transfer/Download/" 1699 | 1700 | if pma._pma_debug == True: 1701 | print("Downloading file {} for slide {}".format( 1702 | relativePath, slideRef)) 1703 | 1704 | params = {"sessionId": sessionID, 1705 | "image": slideRef, "path": relativePath} 1706 | 1707 | with requests.get(pmaCoreDownloadUrl, params=params, stream=True, verify=verify) as r: 1708 | r.raise_for_status() 1709 | 1710 | total = int(r.headers.get('content-length')) 1711 | downloaded = 0 1712 | 1713 | if save_directory: 1714 | filePath = os.path.join(save_directory, relativePath) 1715 | 1716 | dir = os.path.dirname(filePath) 1717 | if not os.path.exists(dir): 1718 | os.makedirs(dir) 1719 | prev = -1 1720 | with open(filePath, 'wb') as f: 1721 | for chunk in r.iter_content(chunk_size=10 * 1024): 1722 | if chunk: 1723 | downloaded += len(chunk) 1724 | f.write(chunk) 1725 | progress = downloaded / total 1726 | if not prev or progress - prev > 0.05 or (progress - prev > 0 and downloaded == total): 1727 | if pma._pma_debug == True: 1728 | print("{0:.0%}".format(progress)) 1729 | prev = progress 1730 | 1731 | 1732 | def dummy_annotation(): 1733 | """Returns a dictionary with the right keys and default values filled out already to be used as input for add_annotation() and add_annotations 1734 | """ 1735 | return {"classification": "", 1736 | "notes": "", 1737 | "geometry": "", # shapely? 1738 | "color": "#000000", 1739 | "fillColor": "#FFFFFF00", 1740 | "lineThickness": 1} 1741 | 1742 | 1743 | def add_annotation(slideRef, classification, notes, ann, color="#000000", layerID=0, sessionID=None, verify=True): 1744 | """Adds an annotation to a slide with the specified parameters 1745 | 1746 | :param slideRef: The slide path to add annotation to 1747 | :type slideRef: str 1748 | :param classification: A string representing the class of this annotation (tumor, necrosis etc) 1749 | :type classification: str 1750 | :param notes: A string for free text notes to be associated with this annotation 1751 | :type notes: str 1752 | :param ann: A Well-Known Text (WKT) representation of the geometry of this annotation; can be a dictionary as well 1753 | :type geometry: str, dict 1754 | :param color: An HTML color, defaults to "#000000" 1755 | :type color: str, optional 1756 | :param layerID: The layer id to attach this annotation to, defaults to 0 1757 | :type layerID: int, optional 1758 | :param sessionID: The PMA.core session id, defaults to None for autodetection 1759 | :type sessionID: str, optional 1760 | :raises ValueError: If the server response is not in a known format 1761 | :return: JSON object containing the annotation ID 1762 | :rtype: int 1763 | """ 1764 | sessionID = _pma_session_id(sessionID) 1765 | if (sessionID == _pma_pmacoreliteSessionID): 1766 | if is_lite(): 1767 | raise ValueError( 1768 | "PMA.core.lite found running, but doesn't support adding annotations.") 1769 | else: 1770 | raise ValueError( 1771 | "PMA.core.lite not found, and besides; it doesn't support adding annotations.") 1772 | 1773 | if not (isinstance(ann, dict) or isinstance(ann, list)): 1774 | geo = ann 1775 | ann = dummy_annotation() 1776 | ann["geometry"] = geo 1777 | ann["color"] = color 1778 | else: 1779 | return 'ann parameter should be WKT(Well-Known Text) string' 1780 | 1781 | url = _pma_api_url(sessionID) + "AddAnnotation" 1782 | 1783 | data = { 1784 | "sessionID": sessionID, 1785 | "pathOrUid": slideRef, 1786 | "classification": classification, 1787 | "layerID": layerID, 1788 | "notes": notes, 1789 | "geometry": ann["geometry"], 1790 | "color": ann["color"] 1791 | } 1792 | 1793 | if pma._pma_debug == True: 1794 | print("url =", url) 1795 | print("payload = ") 1796 | pprint(data) 1797 | 1798 | r = requests.post(url, json=data, verify=verify) 1799 | 1800 | if isinstance(r.json(), int): 1801 | return {'Code': 'Success', 'Message': 'Annotation successfully added', 'annotation_id': r.json()} 1802 | else: 1803 | return r.json() 1804 | 1805 | def add_annotations(slideRef, classification, notes, anns, color="#000000", layerID=0, sessionID=None, verify=True): 1806 | """Adds multiple annotations to a slide with the specified parameters 1807 | 1808 | :param slideRef: The slide path to add annotation to 1809 | :type slideRef: str 1810 | :param classification: A string representing the class of this annotation (tumor, necrosis etc) 1811 | :type classification: str 1812 | :param notes: A string for free text notes to be associated with this annotations. If param Notes is empty the notes parameter of the annotations object will be used 1813 | :type notes: str 1814 | :param anns: A list of Well-Known Text (WKT) representation of the geometry of this annotation. 1815 | anns is an array that contains dictionaries with single annotations values like Classification, LayerID, Notes, 1816 | Geometry, Color, FillColor, lineTickness. 1817 | :type anns: list 1818 | :param color: An HTML color, defaults to "#000000"; you can specify a separate color for each annotation individually as well 1819 | :type color: str, optional 1820 | :param layerID: The layer id to attach this annotation to, defaults to 0 1821 | :type layerID: int, optional 1822 | :param sessionID: The PMA.core session id, defaults to None for autodetection 1823 | :type sessionID: str, optional 1824 | :param verify: confirm whether TLS connection has to be verified 1825 | :type verify: boolean, optional 1826 | :raises ValueError: If the server response is not in a known format 1827 | :return: An integer representing the annotation id 1828 | :rtype: int 1829 | 1830 | Example: 1831 | anns = [] 1832 | annotation = { 1833 | "geometry": string, 1834 | "color": string, 1835 | "fillColor": string, 1836 | "lineThickness": int, 1837 | "Notes": string 1838 | } 1839 | anns.append(annotation) 1840 | 1841 | Notes: 1842 | In case if you want to have different notes per annotation the notes parameter has 1843 | to be empty and the annotation dictionary should contain a notes key: value pair. 1844 | 1845 | """ 1846 | 1847 | if not (type(anns) is list): 1848 | anns = [anns] 1849 | 1850 | sessionID = _pma_session_id(sessionID) 1851 | if (sessionID == _pma_pmacoreliteSessionID): 1852 | if is_lite(): 1853 | raise ValueError( 1854 | "PMA.core.lite found running, but doesn't support adding annotations.") 1855 | else: 1856 | raise ValueError( 1857 | "PMA.core.lite not found, and besides; it doesn't support adding annotations.") 1858 | 1859 | json_all_added_annotations = [] 1860 | for ann in anns: 1861 | 1862 | if not (type(ann) is dict): 1863 | geo = ann 1864 | ann = dummy_annotation() 1865 | ann["geometry"] = geo 1866 | ann["color"] = "#3333FF" 1867 | 1868 | json_single_annotation = { 1869 | "Classification": classification, 1870 | "LayerID": layerID, 1871 | "Notes": notes if notes != "" else ann.get("Notes", ""), 1872 | "Geometry": ann["geometry"], 1873 | "Color": ann["color"], 1874 | "FillColor": ann["fillColor"], 1875 | "LineThickness": ann["lineThickness"] 1876 | } 1877 | json_all_added_annotations.append(json_single_annotation) 1878 | 1879 | data = { 1880 | "sessionID": sessionID, 1881 | "pathOrUid": slideRef, 1882 | "deleted": [], 1883 | "updated": [], 1884 | "added": json_all_added_annotations 1885 | } 1886 | 1887 | url = _pma_api_url(sessionID) + "SaveAnnotations" 1888 | 1889 | if pma._pma_debug == True: 1890 | print("url =", url) 1891 | print("payload = ") 1892 | pprint(data) 1893 | 1894 | r = requests.post(url, json=data, verify=verify) 1895 | 1896 | if pma._pma_debug == True: 1897 | print("HTTP return value = ", r.status_code) 1898 | 1899 | if isinstance(r.json(), list): 1900 | return {'Code': 'Success', 'Message': 'Annotations successfully added', 'annotation_id': r.json()} 1901 | else: 1902 | return r.json() 1903 | 1904 | 1905 | def clear_all_annotations(slideRef, sessionID=None): 1906 | sessionID = _pma_session_id(sessionID) 1907 | if (sessionID == _pma_pmacoreliteSessionID): 1908 | if is_lite(): 1909 | raise ValueError( 1910 | "PMA.core.lite found running, but doesn't support deleting annotations.") 1911 | else: 1912 | raise ValueError( 1913 | "PMA.core.lite not found, and besides; it doesn't support deleting annotations.") 1914 | 1915 | annotations = get_annotations(slideRef, sessionID) 1916 | if annotations is None or annotations == "": 1917 | return True 1918 | 1919 | layerIds = list(set(a["LayerID"] for a in annotations)) 1920 | 1921 | for lId in layerIds: 1922 | clear_annotations(slideRef, lId, sessionID) 1923 | 1924 | return True 1925 | 1926 | 1927 | def clear_annotations(slideRef, layerID, sessionID=None, verify=True): 1928 | sessionID = _pma_session_id(sessionID) 1929 | if (sessionID == _pma_pmacoreliteSessionID): 1930 | if is_lite(): 1931 | raise ValueError( 1932 | "PMA.core.lite found running, but doesn't support deleting annotations.") 1933 | else: 1934 | raise ValueError( 1935 | "PMA.core.lite not found, and besides; it doesn't support deleting annotations.") 1936 | 1937 | url = _pma_api_url(sessionID) + "DeleteAnnotations" 1938 | data = {"sessionID": sessionID, "pathOrUid": slideRef, "layerID": layerID} 1939 | 1940 | r = requests.post(url, json=data, verify=verify) 1941 | if (r.status_code != 200): 1942 | raise Exception("clear_annotation on " + 1943 | slideRef + " resulted in error") 1944 | 1945 | return True 1946 | 1947 | 1948 | def get_annotation_surface_area(slideRef, layerID, annotationID, sessionID=None, verify=True): 1949 | sessionID = _pma_session_id(sessionID) 1950 | if (sessionID == _pma_pmacoreliteSessionID): 1951 | if is_lite(): 1952 | raise ValueError( 1953 | "PMA.core.lite found running, but doesn't support annotations.") 1954 | else: 1955 | raise ValueError( 1956 | "PMA.core.lite not found, and besides; it doesn't support annotations.") 1957 | 1958 | url = _pma_api_url(sessionID) + "GetAnnotationSurfaceArea" 1959 | data = {"sessionID": sessionID, "pathOrUid": slideRef, 1960 | "layerID": layerID, "annotationID": annotationID} 1961 | 1962 | r = requests.get(url, params=data, verify=verify) 1963 | if pma._pma_debug == True: 1964 | print(r.url) 1965 | if (r.status_code != 200): 1966 | raise Exception("get_annotation_surface_area on " + 1967 | slideRef + " resulted in error") 1968 | 1969 | return r.text 1970 | 1971 | 1972 | def get_annotation_distance(slideRef, layerID, annotationID, sessionID=None, verify=True): 1973 | sessionID = _pma_session_id(sessionID) 1974 | if (sessionID == _pma_pmacoreliteSessionID): 1975 | if is_lite(): 1976 | raise ValueError( 1977 | "PMA.core.lite found running, but doesn't support annotations.") 1978 | else: 1979 | raise ValueError( 1980 | "PMA.core.lite not found, and besides; it doesn't support annotations.") 1981 | 1982 | url = _pma_api_url(sessionID) + "GetAnnotationDistance" 1983 | data = {"sessionID": sessionID, "pathOrUid": slideRef, 1984 | "layerID": layerID, "annotationID": annotationID} 1985 | 1986 | r = requests.get(url, params=data, verify=verify) 1987 | if pma._pma_debug == True: 1988 | print(r.url) 1989 | if (r.status_code != 200): 1990 | raise Exception("get_annotation_distance on " + 1991 | slideRef + " resulted in error") 1992 | 1993 | return r.text 1994 | 1995 | -------------------------------------------------------------------------------- /pma_python/core_admin.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pma_python import core, pma 3 | from math import ceil 4 | from PIL import Image 5 | from random import choice 6 | from io import BytesIO 7 | from urllib.parse import quote 8 | from urllib.request import urlopen 9 | 10 | import requests 11 | 12 | __version__ = pma.__version__ 13 | 14 | 15 | def set_debug_flag(flag): 16 | """ 17 | Determine whether pma_python runs in debugging mode or not. 18 | When in debugging mode (flag = true), extra output is produced when certain conditions in the code are not met 19 | """ 20 | pma._pma_set_debug_flag(flag) 21 | 22 | 23 | def _pma_admin_url(sessionID=None): 24 | # let's get the base URL first for the specified session 25 | url = core._pma_url(sessionID) 26 | if url is None: 27 | # sort of a hopeless situation; there is no URL to refer to 28 | return None 29 | # remember, _pma_url is guaranteed to return a URL that ends with "/" 30 | return pma._pma_join(url, "admin/json/") 31 | 32 | 33 | def _pma_check_for_pma_start(method="", url=None, session=None): 34 | if (core._pma_pmacoreliteSessionID == session): 35 | raise Exception("PMA.start doesn't support", method) 36 | elif (url == core._pma_pmacoreliteURL): 37 | if core.is_lite(): 38 | raise ValueError( 39 | "PMA.core.lite found running, but doesn't support an administrative back-end") 40 | else: 41 | raise ValueError( 42 | "PMA.core.lite not found, and besides; it doesn't support an administrative back-end anyway") 43 | 44 | 45 | def _pma_http_post(url, data, verify=True): 46 | if (pma._pma_debug is True): 47 | print("Posting to", url) 48 | print(" with payload", data) 49 | resp = requests.post(url, json=data, verify=verify) 50 | if pma._pma_debug is True and "code" in resp.text: 51 | print(resp.text) 52 | else: 53 | pma._pma_clear_url_cache() 54 | return resp.text 55 | 56 | 57 | def admin_connect(pmacoreURL, pmacoreAdmUsername, pmacoreAdmPassword): 58 | """ 59 | Attempt to connect to PMA.core instance; success results in a SessionID 60 | only success if the user has administrative status 61 | """ 62 | _pma_check_for_pma_start("admin_connect", pmacoreURL) 63 | 64 | url = pma._pma_join( 65 | pmacoreURL, "admin/json/AdminAuthenticate?caller=SDK.Python") 66 | url += "&username=" + pma._pma_q(pmacoreAdmUsername) 67 | url += "&password=" + pma._pma_q(pmacoreAdmPassword) 68 | 69 | if (pma._pma_debug is True): 70 | print(url) 71 | 72 | try: 73 | headers = {'Accept': 'application/json'} 74 | # r = requests.get(url, headers=headers) 75 | r = pma._pma_http_get(url, headers) 76 | except Exception as e: 77 | print(e) 78 | return None 79 | loginresult = r.json() 80 | 81 | # print(loginresult) 82 | 83 | if (str(loginresult["Success"]).lower() != "true"): 84 | admSessionID = None 85 | else: 86 | admSessionID = loginresult["SessionId"] 87 | 88 | core._pma_sessions[admSessionID] = pmacoreURL 89 | core._pma_usernames[admSessionID] = pmacoreAdmUsername 90 | 91 | if not (admSessionID in core._pma_slideinfos): 92 | core._pma_slideinfos[admSessionID] = dict() 93 | core._pma_amount_of_data_downloaded[admSessionID] = len(loginresult) 94 | 95 | return (admSessionID) 96 | 97 | 98 | def admin_disconnect(admSessionID=None): 99 | """ 100 | Attempt to disconnect from PMA.core instance; True if valid admSessionID was indeed disconnected 101 | """ 102 | return core.disconnect(admSessionID) 103 | 104 | 105 | def send_email_reminder(admSessionID, login, subject="PMA.core password reminder"): 106 | """ 107 | Send out an email reminder to the address associated with user login 108 | """ 109 | reminderParams = {"username": login, 110 | "subject": subject, "messageTemplate": ""} 111 | url = _pma_admin_url(admSessionID) + "EmailPassword" 112 | reminderResponse = _pma_http_post(url, reminderParams) 113 | return reminderResponse 114 | 115 | 116 | def add_user(admSessionID, login, firstName, lastName, email, pwd, canAnnotate=False, isAdmin=False, isSuspended=False): 117 | print("Using credentials from ", admSessionID) 118 | 119 | createUserParams = { 120 | "sessionID": admSessionID, 121 | "user": { 122 | "Login": login, 123 | "FirstName": firstName, 124 | "LastName": lastName, 125 | "Password": pwd, 126 | "Email": email, 127 | "Administrator": isAdmin, 128 | "CanAnnotate": canAnnotate, 129 | "Suspended": isSuspended 130 | } 131 | } 132 | url = _pma_admin_url(admSessionID) + "CreateUser" 133 | createUserResponse = _pma_http_post(url, createUserParams) 134 | return createUserResponse 135 | 136 | 137 | def user_exists(admSessionID, u): 138 | from pma_python import pma 139 | url = (_pma_admin_url(admSessionID) + "SearchUsers?source=Local" + 140 | "&SessionID=" + pma._pma_q(admSessionID) + "&query=" + pma._pma_q(u)) 141 | try: 142 | r = pma._pma_http_get(url, {'Accept': 'application/json'}) 143 | except Exception as e: 144 | print(e) 145 | return None 146 | results = r.json() 147 | for usr in results: 148 | if usr["Login"].lower() == u.lower(): 149 | return True 150 | return False 151 | 152 | 153 | def create_amazons3_mounting_point(accessKey, secretKey, path, instanceId, chunkSize=1048576, serviceUrl=None): 154 | """ 155 | create an Amazon S3 mounting point. A list of these is to be used to supply method create_root_directory() 156 | """ 157 | createAmazonS3MountingPointParams = { 158 | "AccessKey": accessKey, 159 | "SecretKey": secretKey, 160 | "ChunkSize": chunkSize, 161 | "ServiceUrl": serviceUrl, 162 | "Path": path, 163 | "InstanceId": instanceId 164 | } 165 | return createAmazonS3MountingPointParams 166 | 167 | 168 | def create_filesystem_mounting_point(username, password, domainName, path, instanceId): 169 | """ 170 | create an FileSystem mounting point. A list of these is to be used to supply method create_root_directory() 171 | """ 172 | createFileSystemMountingPointParams = { 173 | "Username": username, 174 | "Password": password, 175 | "DomainName": domainName, 176 | "Path": path, 177 | "InstanceId": instanceId 178 | } 179 | return createFileSystemMountingPointParams 180 | 181 | 182 | def create_onedrive_mounting_point(): 183 | """ 184 | Placeholder for future functionality 185 | """ 186 | return None 187 | 188 | 189 | def create_dropbox_mounting_point(): 190 | """ 191 | Placeholder for future functionality 192 | """ 193 | return None 194 | 195 | 196 | def create_googledrive_mounting_point(): 197 | """ 198 | Placeholder for future functionality 199 | """ 200 | return None 201 | 202 | 203 | def create_root_directory(admSessionID, 204 | alias, 205 | amazonS3MountingPoints=None, 206 | fileSystemMountingPoints=None, 207 | description="Root dir created through pma_python", 208 | isPublic=False, 209 | isOffline=False, 210 | verify=True): 211 | createRootDirectoryParams = { 212 | "sessionID": admSessionID, 213 | "rootDirectory": { 214 | "Alias": alias, 215 | "Description": description, 216 | "Public": isPublic, 217 | "Offline": isOffline, 218 | "AmazonS3MountingPoints": amazonS3MountingPoints, 219 | "FileSystemMountingPoints": fileSystemMountingPoints 220 | } 221 | } 222 | url = _pma_admin_url(admSessionID) + "CreateRootDirectory" 223 | createRootDirectoryReponse = requests.post( 224 | url, json=createRootDirectoryParams, verify=verify) 225 | return createRootDirectoryReponse.text 226 | 227 | 228 | def create_directory(admSessionID, path): 229 | try: 230 | slides = core.get_slides(path) 231 | if pma._pma_debug is True: 232 | print("Directory already exists") 233 | return False 234 | except Exception: 235 | url = _pma_admin_url(admSessionID) + "CreateDirectory" 236 | result = _pma_http_post(url, {"sessionID": admSessionID, "path": path}) 237 | 238 | try: 239 | return len(core.get_slides(path)) == 0 240 | except Exception: 241 | return False 242 | 243 | 244 | def rename_directory(admSessionID, originalPath, newName): 245 | url = _pma_admin_url(admSessionID) + "RenameDirectory" 246 | payload = {"sessionID": admSessionID, 247 | "path": originalPath, "newName": newName} 248 | result = _pma_http_post(url, payload) 249 | if "Code" in result: 250 | if pma._pma_debug is True: 251 | print(result) 252 | return False 253 | return True 254 | 255 | 256 | def delete_directory(admSessionID, path): 257 | """ 258 | Deletes a directory from the PMA.core storage 259 | """ 260 | url = _pma_admin_url(admSessionID) + "DeleteDirectory" 261 | payload = { 262 | "sessionID": admSessionID, 263 | "path": path, 264 | } 265 | result = _pma_http_post(url, payload) 266 | if "Code" in result: 267 | if pma._pma_debug is True: 268 | print(result) 269 | return False 270 | return True 271 | 272 | 273 | def delete_slide(admSessionID, slideRef): 274 | """ 275 | Deletes a slide from the PMA.core storage 276 | """ 277 | url = _pma_admin_url(admSessionID) + "DeleteSlide" 278 | payload = { 279 | "sessionID": admSessionID, 280 | "path": slideRef, 281 | } 282 | result = _pma_http_post(url, payload) 283 | if "Code" in result: 284 | if pma._pma_debug is True: 285 | print(result) 286 | return False 287 | return True 288 | 289 | 290 | def reverse_uid(admSessionID, slideRefUid, verify=True): 291 | """ 292 | lookup the reverse path of a UID for a specific slide 293 | """ 294 | if (admSessionID == core._pma_pmacoreliteSessionID): 295 | if is_lite(): 296 | raise ValueError( 297 | "PMA.core.lite found running, but doesn't support UIDs. For advanced anonymization, please upgrade to PMA.core." 298 | ) 299 | else: 300 | raise ValueError( 301 | "PMA.core.lite not found, and besides; it doesn't support UIDs. For advanced anonymization, please upgrade to PMA.core." 302 | ) 303 | url = _pma_admin_url(admSessionID) + "ReverseLookupUID?sessionID=" + \ 304 | pma._pma_q(admSessionID) + "&uid=" + pma._pma_q(slideRefUid) 305 | if (pma._pma_debug is True): 306 | print(url) 307 | r = requests.get(url, verify=verify) 308 | json = r.json() 309 | if ("Code" in json): 310 | raise Exception("reverse_uid on " + slideRefUid + 311 | " resulted in: " + json["Message"]) 312 | else: 313 | path = json 314 | return path 315 | 316 | 317 | def reverse_root_directory(admSessionID, alias, verify=True): 318 | """ 319 | lookup the reverse path of a root-directory 320 | """ 321 | if (admSessionID == core._pma_pmacoreliteSessionID): 322 | if is_lite(): 323 | raise ValueError( 324 | "PMA.core.lite found running, but doesn't support this method." 325 | ) 326 | else: 327 | raise ValueError( 328 | "PMA.core.lite not found, and besides; it doesn't support this method." 329 | ) 330 | url = _pma_admin_url(admSessionID) + "ReverseLookupRootDirectory?sessionID=" + \ 331 | pma._pma_q(admSessionID) + "&alias=" + pma._pma_q(alias) 332 | if (pma._pma_debug is True): 333 | print(url) 334 | r = requests.get(url, verify=verify) 335 | json = r.json() 336 | if ("Code" in json): 337 | raise Exception("reverse_root_directory on " + 338 | alias + " resulted in: " + json["Message"]) 339 | else: 340 | path = json 341 | return path 342 | -------------------------------------------------------------------------------- /pma_python/pma.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join 3 | from urllib.parse import quote 4 | from pma_python import version 5 | 6 | import requests 7 | 8 | __version__ = version.__version__ 9 | 10 | _pma_url_content = {} 11 | _pma_debug = False 12 | 13 | 14 | def _pma_join(*s): 15 | joinstring = "" 16 | for ss in s: 17 | if not (ss is None): 18 | joinstring = join(joinstring, ss) 19 | return joinstring.replace("\\", "/") 20 | 21 | 22 | def _pma_q(arg): 23 | if (arg is None): 24 | return '' 25 | else: 26 | return quote(str(arg), safe='') 27 | 28 | 29 | def _pma_http_get(url, headers, verify=True): 30 | global _pma_url_content 31 | global _pma_debug 32 | 33 | if not (url in _pma_url_content): 34 | if _pma_debug is True: 35 | print("Retrieving ", url) 36 | r = requests.get(url, headers=headers, verify=verify) 37 | _pma_url_content[url] = r 38 | 39 | return _pma_url_content[url] 40 | 41 | 42 | def _pma_clear_url_cache(): 43 | global _pma_url_content 44 | _pma_url_content = {} 45 | 46 | 47 | def _pma_set_debug_flag(flag): 48 | """ 49 | Determine whether pma_python runs in debugging mode or not. 50 | When in debugging mode (flag = true), extra output is produced when certain conditions in the code are not met 51 | """ 52 | global _pma_debug 53 | 54 | if not isinstance(flag, (bool)): 55 | raise Exception("flag argument must be of class bool") 56 | _pma_debug = flag 57 | if flag is True: 58 | print("Debug flag enabled. You will receive extra feedback and messages from pma_python (like this one)") 59 | 60 | 61 | def get_supported_formats(pandas=False, verify=True): 62 | """ 63 | Get an up-to-date list of all supported file formats on the Pathomation software platform 64 | """ 65 | global _pma_debug 66 | url = "https://host.pathomation.com/etc/supported_formats.php" 67 | 68 | if _pma_debug == True: 69 | print(url) 70 | 71 | headers = {'Accept': 'application/json'} 72 | r = requests.get(url, headers=headers, verify=verify) 73 | json = r.json() 74 | 75 | if (pandas == True): 76 | import pandas as pd 77 | return pd.DataFrame.from_records(json, index=["vendor"]) 78 | else: 79 | return json 80 | -------------------------------------------------------------------------------- /pma_python/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.0.0.178' 2 | -------------------------------------------------------------------------------- /pma_python/view.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib.parse import quote 3 | from urllib.request import urlopen 4 | from pma_python import pma 5 | 6 | import requests 7 | 8 | __version__ = pma.__version__ 9 | 10 | 11 | def set_debug_flag(flag): 12 | """ 13 | Determine whether pma_python runs in debugging mode or not. 14 | When in debugging mode (flag = true), extra output is produced when certain conditions in the code are not met 15 | """ 16 | pma._pma_set_debug_flag(flag) 17 | 18 | 19 | def get_version_info(pmaviewURL): 20 | """ 21 | Get version info from PMA.view instance running at pmaviewURL 22 | """ 23 | # purposefully DON'T use helper function _pma_api_url() here: 24 | # why? because GetVersionInfo can be invoked WITHOUT a valid SessionID; 25 | # _pma_api_url() takes session information into account 26 | url = pma._pma_join(pmaviewURL, "api/json/GetVersionInfo") 27 | version = "" 28 | try: 29 | # Are we looking at PMA.view/studio 2.x? 30 | if pma._pma_debug is True: 31 | print(url) 32 | contents = urlopen(url).read().decode("utf-8").strip("\"").strip("'") 33 | return contents 34 | except Exception as e: 35 | version = None 36 | 37 | url = pma._pma_join(pmaviewURL, "viewer/version") 38 | try: 39 | # Oops, perhaps this is a PMA.view 1.x version 40 | if pma._pma_debug is True: 41 | print(url) 42 | contents = urlopen(url).read().decode("utf-8").strip("\"").strip("'") 43 | return contents 44 | except Exception as e: 45 | version = None 46 | 47 | return version 48 | -------------------------------------------------------------------------------- /samples/ConvertToTiff.py: -------------------------------------------------------------------------------- 1 | # Uncomment below to download the required GDAL library 2 | # from IPython import get_ipython 3 | # # Download the required GDAL package from the [Unofficial Windows Binaries for Python Extension Packages](https://www.lfd.uci.edu/~gohlke/pythonlibs/) 4 | # import sys 5 | # is_64bits = sys.maxsize > 2**32 6 | # gdalDownloadLink = "https://download.lfd.uci.edu/pythonlibs/s2jqpv5t/GDAL-3.0.4-cp{0}-cp{0}m-{1}.whl".format(str(sys.version_info.major) + str(sys.version_info.minor), "win_amd64" if is_64bits else "win32") 7 | # get_ipython().system('pip install {gdalDownloadLink}') 8 | 9 | from pma_python import * 10 | import os 11 | import numpy as np 12 | from osgeo import gdal 13 | import math 14 | import tqdm 15 | 16 | _pmaCoreUrl = "http://localhost:54001/" 17 | 18 | # what slide do you want to convert? 19 | slidePath = "C:/Slides/Slide.svs" 20 | # set the target TIFF quality 0-100 21 | target_quality = 100 22 | # set the target scale factor to download. One of [1, 2, 4, 8, 16, 32, 64, 128] 23 | downscale_factor = 1 24 | 25 | # Get the slide information and information about each zoomlevel available 26 | 27 | print("Fetching image info for {0}".format(slidePath)) 28 | slideInfo = core.get_slide_info(slidePath) 29 | print(slideInfo) 30 | zoomLevelsInfo = core.get_zoomlevels_dict(slidePath) 31 | maxLevel = max(zoomLevelsInfo) 32 | tileSize = slideInfo["TileSize"] 33 | print("Horizontal Tiles | Vertical Tiles | Total Tiles") 34 | for level in zoomLevelsInfo: 35 | tilesX, tilesY, totalTiles = zoomLevelsInfo[level] 36 | print("{:>16} |{:>15} |{:>12}".format(tilesX, tilesY, totalTiles)) 37 | 38 | filename = slidePath.rpartition("/")[-1] 39 | xresolution = 10000 / slideInfo["MicrometresPerPixelX"] 40 | yresolution = 10000 / slideInfo["MicrometresPerPixelY"] 41 | 42 | # Create new TIFF file using the GDAL TIFF driver 43 | # The width and height of the final tiff is based on number of tiles horizontally and vertically. 44 | 45 | # Validate the parameters 46 | if target_quality is None or target_quality < 0 or target_quality > 90: 47 | target_quality = 80 48 | if downscale_factor not in [1, 2, 4, 8, 16, 32, 64, 128]: 49 | downscale_factor = 1 50 | 51 | 52 | maxLevel = max(zoomLevelsInfo) 53 | powerof2 = int(math.log2(downscale_factor)) 54 | 55 | level = maxLevel - powerof2 56 | level = min(max(level, 0), maxLevel) 57 | tilesX, tilesY, totalTiles = zoomLevelsInfo[level] 58 | 59 | # We set the region of the image we want to read to set the final tif size accordingly 60 | tileRegionX = (0, tilesX) 61 | tileRegionY = (0, tilesY) 62 | 63 | tileSize = 512 64 | tiff_drv = gdal.GetDriverByName("GTiff") 65 | # Set the final size 66 | ds = tiff_drv.Create( 67 | filename.split('.')[0] + '.tif', 68 | int((tileRegionX[1] - tileRegionX[0]) * 512), 69 | int((tileRegionY[1] - tileRegionY[0]) * 512), 70 | 3, 71 | options=['BIGTIFF=YES', 72 | 'COMPRESS=JPEG', 'TILED=YES', 'BLOCKXSIZE=' + str(tileSize), 'BLOCKYSIZE=' + str(tileSize), 73 | 'JPEG_QUALITY=90', 'PHOTOMETRIC=RGB' 74 | ]) 75 | descr = "ImageJ=\nhyperstack=true\nimages=1\nchannels=1\nslices=1\nframes=1" 76 | ds.SetMetadata({ 'TIFFTAG_RESOLUTIONUNIT': '3', 'TIFFTAG_XRESOLUTION': str(int(xresolution / downscale_factor)), 'TIFFTAG_YRESOLUTION': str(int(yresolution / downscale_factor)), 'TIFFTAG_IMAGEDESCRIPTION': descr }) 77 | 78 | 79 | print("Maximum level = ", maxLevel, ", level = ", level, ", power of 2 = ", powerof2) 80 | filename.split('.')[0] + '.tif' 81 | 82 | # We read each tile of the final zoomlevel (1:1 resolution) from the server and write it to the resulting TIFF file 83 | # Then we create the pyramid of the file using BuildOverviews function of GDAL 84 | tilesX, tilesY, totalTiles = zoomLevelsInfo[level] 85 | print("Requesting level {}".format(level)) 86 | 87 | pbar = tqdm.tqdm(total= int((tileRegionX[1] - tileRegionX[0])*(tileRegionY[1] - tileRegionY[0]))) 88 | for x in range(tileRegionX[0], tileRegionX[1]): 89 | for y in range(tileRegionY[0],tileRegionY[1], 1): # range of y-axis in which we are interested for this slide 90 | pbar.update() 91 | tile = core.get_tile(slidePath, x, y , level, quality=target_quality) 92 | arr = np.array(tile, np.uint8) 93 | 94 | # calculate startx starty pixel coordinates based on tile indexes (x,y) 95 | # for the final tif we want the first tile, i.e. (tileRegionX[0], tileRegionY[0]) ,to be at (0,0) so we need to transform the coordinates 96 | sx = (x - tileRegionX[0]) * tileSize 97 | sy = (y - tileRegionY[0]) * tileSize 98 | 99 | ds.GetRasterBand(1).WriteArray(arr[..., 0], sx, sy) 100 | ds.GetRasterBand(2).WriteArray(arr[..., 1], sx, sy) 101 | ds.GetRasterBand(3).WriteArray(arr[..., 2], sx, sy) 102 | 103 | pbar.close() 104 | print("Please wait while building the pyramid") 105 | ds.BuildOverviews('average', [pow(2, l) for l in range(1, level)]) 106 | ds = None 107 | print("Done") 108 | 109 | 110 | -------------------------------------------------------------------------------- /samples/jupyter/ConvertToTiff.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Download the required GDAL package from the [Unofficial Windows Binaries for Python Extension Packages](https://www.lfd.uci.edu/~gohlke/pythonlibs/)" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import sys\n", 17 | "is_64bits = sys.maxsize > 2**32\n", 18 | "gdalDownloadLink = \"https://download.lfd.uci.edu/pythonlibs/s2jqpv5t/GDAL-3.0.4-cp{0}-cp{0}m-{1}.whl\".format(str(sys.version_info.major) + str(sys.version_info.minor), \"win_amd64\" if is_64bits else \"win32\")\n", 19 | "!pip install {gdalDownloadLink}" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "from pma_python import *\n", 29 | "import os\n", 30 | "import numpy as np\n", 31 | "from osgeo import gdal\n", 32 | "import math\n", 33 | "import tqdm\n", 34 | "\n", 35 | "_pmaCoreUrl = \"http://localhost:54001/\"\n", 36 | "\n", 37 | "# what slide do you want to convert?\n", 38 | "slidePath = \"C:/Slides/Slide.svs\"\n", 39 | "# set the target TIFF quality 0-100\n", 40 | "target_quality = 100\n", 41 | "# set the target scale factor to download. One of [1, 2, 4, 8, 16, 32, 64, 128]\n", 42 | "downscale_factor = 1" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "Get the slide information and information about each zoomlevel available" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "print(\"Fetching image info for {0}\".format(slidePath))\n", 59 | "slideInfo = core.get_slide_info(slidePath)\n", 60 | "print(slideInfo)\n", 61 | "zoomLevelsInfo = core.get_zoomlevels_dict(slidePath)\n", 62 | "maxLevel = max(zoomLevelsInfo)\n", 63 | "tileSize = slideInfo[\"TileSize\"]\n", 64 | "print(\"Horizontal Tiles | Vertical Tiles | Total Tiles\")\n", 65 | "for level in zoomLevelsInfo:\n", 66 | " tilesX, tilesY, totalTiles = zoomLevelsInfo[level]\n", 67 | " print(\"{:>16} |{:>15} |{:>12}\".format(tilesX, tilesY, totalTiles))\n", 68 | "\n", 69 | "filename = slidePath.rpartition(\"/\")[-1]\n", 70 | "xresolution = 10000 / slideInfo[\"MicrometresPerPixelX\"]\n", 71 | "yresolution = 10000 / slideInfo[\"MicrometresPerPixelY\"]\n" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "filename" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "Create new TIFF file using the GDAL TIFF driver\n", 88 | "The width and height of the final tiff is based on number of tiles horizontally and vertically." 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "# Validate the parameters\n", 98 | "if target_quality is None or target_quality < 0 or target_quality > 90:\n", 99 | " target_quality = 80\n", 100 | "if downscale_factor not in [1, 2, 4, 8, 16, 32, 64, 128]:\n", 101 | " downscale_factor = 1" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "maxLevel = max(zoomLevelsInfo)\n", 111 | "powerof2 = int(math.log2(downscale_factor))\n", 112 | "\n", 113 | "level = maxLevel - powerof2\n", 114 | "level = min(max(level, 0), maxLevel)\n", 115 | "tilesX, tilesY, totalTiles = zoomLevelsInfo[level]\n", 116 | "\n", 117 | "# We set the region of the image we want to read to set the final tif size accordingly\n", 118 | "tileRegionX = (0, tilesX)\n", 119 | "tileRegionY = (0, tilesY)\n", 120 | "\n", 121 | "tileSize = 512\n", 122 | "tiff_drv = gdal.GetDriverByName(\"GTiff\")\n", 123 | "# Set the final size\n", 124 | "ds = tiff_drv.Create(\n", 125 | " filename.split('.')[0] + '.tif',\n", 126 | " int((tileRegionX[1] - tileRegionX[0]) * 512),\n", 127 | " int((tileRegionY[1] - tileRegionY[0]) * 512),\n", 128 | " 3,\n", 129 | " options=['BIGTIFF=YES',\n", 130 | " 'COMPRESS=JPEG', 'TILED=YES', 'BLOCKXSIZE=' + str(tileSize), 'BLOCKYSIZE=' + str(tileSize),\n", 131 | " 'JPEG_QUALITY=90', 'PHOTOMETRIC=RGB'\n", 132 | " ])\n", 133 | "descr = \"ImageJ=\\nhyperstack=true\\nimages=1\\nchannels=1\\nslices=1\\nframes=1\"\n", 134 | "ds.SetMetadata({ 'TIFFTAG_RESOLUTIONUNIT': '3', 'TIFFTAG_XRESOLUTION': str(int(xresolution / downscale_factor)), 'TIFFTAG_YRESOLUTION': str(int(yresolution / downscale_factor)), 'TIFFTAG_IMAGEDESCRIPTION': descr })" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "print(\"Maximum level = \", maxLevel, \", level = \", level, \", power of 2 = \", powerof2)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "filename.split('.')[0] + '.tif'" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "We read each tile of the final zoomlevel (1:1 resolution) from the server and write it to the resulting TIFF file\n", 160 | "Then we create the pyramid of the file using BuildOverviews function of GDAL" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "tilesX, tilesY, totalTiles = zoomLevelsInfo[level]\n", 170 | "print(\"Requesting level {}\".format(level))\n", 171 | "pbar = tqdm.tqdm(total= int((tileRegionX[1] - tileRegionX[0])*(tileRegionY[1] - tileRegionY[0]))) \n", 172 | "for x in range(tileRegionX[0], tileRegionX[1]):\n", 173 | " for y in range(tileRegionY[0],tileRegionY[1], 1): # range of y-axis in which we are interested for this slide\n", 174 | " pbar.update()\n", 175 | " tile = core.get_tile(slidePath, x, y , level, quality=target_quality)\n", 176 | " arr = np.array(tile, np.uint8)\n", 177 | "\n", 178 | " # calculate startx starty pixel coordinates based on tile indexes (x,y)\n", 179 | " # for the final tif we want the first tile, i.e. (tileRegionX[0], tileRegionY[0]) ,to be at (0,0) so we need to transform the coordinates\n", 180 | " sx = (x - tileRegionX[0]) * tileSize\n", 181 | " sy = (y - tileRegionY[0]) * tileSize\n", 182 | "\n", 183 | " ds.GetRasterBand(1).WriteArray(arr[..., 0], sx, sy)\n", 184 | " ds.GetRasterBand(2).WriteArray(arr[..., 1], sx, sy)\n", 185 | " ds.GetRasterBand(3).WriteArray(arr[..., 2], sx, sy)\n", 186 | "\n", 187 | "pbar.close()\n", 188 | "print(\"Please wait while building the pyramid\")\n", 189 | "ds.BuildOverviews('average', [pow(2, l) for l in range(1, level)])\n", 190 | "ds = None\n", 191 | "print(\"Done\")" 192 | ] 193 | } 194 | ], 195 | "metadata": { 196 | "file_extension": ".py", 197 | "kernelspec": { 198 | "display_name": "Python 3", 199 | "language": "python", 200 | "name": "python3" 201 | }, 202 | "language_info": { 203 | "codemirror_mode": { 204 | "name": "ipython", 205 | "version": 3 206 | }, 207 | "file_extension": ".py", 208 | "mimetype": "text/x-python", 209 | "name": "python", 210 | "nbconvert_exporter": "python", 211 | "pygments_lexer": "ipython3", 212 | "version": "3.8.3-final" 213 | }, 214 | "mimetype": "text/x-python", 215 | "name": "python", 216 | "npconvert_exporter": "python", 217 | "pygments_lexer": "ipython3", 218 | "version": 3 219 | }, 220 | "nbformat": 4, 221 | "nbformat_minor": 2 222 | } -------------------------------------------------------------------------------- /samples/jupyter/PMA.core.admin.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Setup" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Use pip to download and install the necessary libraries if needed" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "!pip install --upgrade pma_python\n", 24 | "!pip install --upgrade pprint" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "Import libraries and set connection parameters" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "# helper libraries\n", 41 | "import pprint as pp # pretty print library is better to print list and dictionary structures\n", 42 | "import random\n", 43 | "\n", 44 | "# pma_python\n", 45 | "from pma_python import core_admin as ca\n", 46 | "from pma_python import core\n", 47 | "print(\"pma_python library loaded; version\", core.__version__)\n", 48 | "\n", 49 | "# connection parameters to be used throughout this notebook\n", 50 | "pma_core_server = \"https://host.pathomation.com/sandbox/2/PMA.core/\"\n", 51 | "pma_core_user = \"user1\"\n", 52 | "pma_core_pass = \"Pathomation\"\n", 53 | "pma_core_slide_dir = \"hgx_cases\"\n", 54 | "\n", 55 | "local_path = \"\"\n", 56 | "s3_key = \"\"\n", 57 | "s3_secret = \"\"\n", 58 | "s3_path = \"\"\n", 59 | "\n", 60 | "if not core.is_lite(pma_core_server):\n", 61 | " print (\"PMA.core found. Good\")\n", 62 | "else:\n", 63 | " raise Exception(\"Unable to detect PMA.core! Please update configuration parameters in this block\")" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "# only needed when debugging code for extra error messages:\n", 73 | "ca.set_debug_flag(True)" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "# PMA.core administration examples" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "Example 500: admin connect" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "print(\"Logging in with an 'administrative' user account\")\n", 97 | "sessionID = ca.admin_connect(pma_core_server, pma_core_user, pma_core_pass)\n", 98 | "print(\"Administrative SessionID\", sessionID)" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "Example 510: add user" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "new_user = \"user\" + str(random.randint(10,100))\n", 115 | "new_pass = \"pass\" + str(random.randint(10,100))\n", 116 | "\n", 117 | "user = ca.add_user(sessionID, new_user, \"John\", \"Doe\", new_user+\"@doe.family\", new_pass);\n", 118 | "print(user)\n", 119 | "new_session = core.connect(pma_core_server, new_user, new_pass);\n", 120 | "print (\"SessionID obtained for\", new_user, \":\", new_session)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "Example 520: user exists" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [] 143 | } 144 | ], 145 | "metadata": { 146 | "kernelspec": { 147 | "display_name": "Python 3", 148 | "language": "python", 149 | "name": "python3" 150 | }, 151 | "language_info": { 152 | "codemirror_mode": { 153 | "name": "ipython", 154 | "version": 3 155 | }, 156 | "file_extension": ".py", 157 | "mimetype": "text/x-python", 158 | "name": "python", 159 | "nbconvert_exporter": "python", 160 | "pygments_lexer": "ipython3", 161 | "version": "3.7.2" 162 | } 163 | }, 164 | "nbformat": 4, 165 | "nbformat_minor": 2 166 | } 167 | -------------------------------------------------------------------------------- /samples/jupyter/PMA.start.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Setup\n", 8 | "Use pip to download and install the necessary libraries if needed" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "!pip install --upgrade pma_python\n", 18 | "!pip install --upgrade pprint" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [ 26 | { 27 | "name": "stdout", 28 | "output_type": "stream", 29 | "text": [ 30 | "pma_python library loaded; version 2.0.0.113\n" 31 | ] 32 | }, 33 | { 34 | "ename": "Exception", 35 | "evalue": "Unable to detect PMA.start! Please start PMA.start, or download it from http://free.pathomation.com", 36 | "output_type": "error", 37 | "traceback": [ 38 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 39 | "\u001b[1;31mException\u001b[0m Traceback (most recent call last)", 40 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 21\u001b[0m \u001b[0mprint\u001b[0m \u001b[1;33m(\u001b[0m\u001b[1;34m\"Connected to PMA.start\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 22\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 23\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Unable to detect PMA.start! Please start PMA.start, or download it from http://free.pathomation.com\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", 41 | "\u001b[1;31mException\u001b[0m: Unable to detect PMA.start! Please start PMA.start, or download it from http://free.pathomation.com" 42 | ] 43 | } 44 | ], 45 | "source": [ 46 | "#\n", 47 | "# helper libraries\n", 48 | "#\n", 49 | "import pprint as pp # pretty print library is better to print list and dictionary structures\n", 50 | "import os\n", 51 | "import matplotlib.pyplot as plt\n", 52 | "import numpy as np\n", 53 | "from PIL import Image\n", 54 | "import pandas as pd\n", 55 | "\n", 56 | "#\n", 57 | "# pma_python library\n", 58 | "#\n", 59 | "from pma_python import core\n", 60 | "print(\"pma_python library loaded; version\", core.__version__)\n", 61 | "\n", 62 | "# connection parameters to be used throughout this notebook\n", 63 | "pma_start_slide_dir = \"C:/wsi\"\n", 64 | "\n", 65 | "if core.is_lite():\n", 66 | " print (\"Connected to PMA.start\")\n", 67 | "else:\n", 68 | " raise Exception(\"Unable to detect PMA.start! Please start PMA.start, or download it from http://free.pathomation.com\")" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "# only needed when debugging code for extra error messages:\n", 78 | "core.set_debug_flag(True)" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 2, 84 | "metadata": {}, 85 | "outputs": [ 86 | { 87 | "name": "stdout", 88 | "output_type": "stream", 89 | "text": [ 90 | "dict_keys([])\n" 91 | ] 92 | } 93 | ], 94 | "source": [ 95 | "print(core._pma_slideinfos.keys())" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "# PMA.start examples\n", 103 | "example 10: identifying PMA.start" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 4, 109 | "metadata": {}, 110 | "outputs": [ 111 | { 112 | "name": "stdout", 113 | "output_type": "stream", 114 | "text": [ 115 | "Are you running PMA.start? True\n", 116 | "Are you running PMA.start at https://host.pathomation.com/sandbox/2/pma.core? False\n" 117 | ] 118 | }, 119 | { 120 | "ename": "JSONDecodeError", 121 | "evalue": "Expecting value: line 1 column 1 (char 0)", 122 | "output_type": "error", 123 | "traceback": [ 124 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 125 | "\u001b[1;31mJSONDecodeError\u001b[0m Traceback (most recent call last)", 126 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[1;31m# testing against a non-existing end-point\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 9\u001b[0m \u001b[0mpma_core_location\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;34m\"https://www.google.com\"\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 10\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Are you running PMA.start at \"\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mpma_core_location\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;34m\"? \"\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mstr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcore\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mis_lite\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mpma_core_location\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", 127 | "\u001b[1;32mc:\\projects\\pathomation-sdk\\version 2\\python\\pma_python\\pma_python\\core.py\u001b[0m in \u001b[0;36mis_lite\u001b[1;34m(pmacoreURL)\u001b[0m\n\u001b[0;32m 135\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mresults\u001b[0m \u001b[1;32min\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 136\u001b[0m \"\"\"\n\u001b[1;32m--> 137\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0m_pma_is_lite\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mpmacoreURL\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 138\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 139\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", 128 | "\u001b[1;32mc:\\projects\\pathomation-sdk\\version 2\\python\\pma_python\\pma_python\\core.py\u001b[0m in \u001b[0;36m_pma_is_lite\u001b[1;34m(pmacoreURL)\u001b[0m\n\u001b[0;32m 98\u001b[0m \u001b[1;31m# this happens when NO instance of PMA.core is detected\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 99\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 100\u001b[1;33m \u001b[0mvalue\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mr\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mjson\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 101\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mvalue\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mTrue\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 102\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", 129 | "\u001b[1;32mc:\\users\\antreas\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\requests\\models.py\u001b[0m in \u001b[0;36mjson\u001b[1;34m(self, **kwargs)\u001b[0m\n\u001b[0;32m 895\u001b[0m \u001b[1;31m# used.\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 896\u001b[0m \u001b[1;32mpass\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 897\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mcomplexjson\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mloads\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtext\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 898\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 899\u001b[0m \u001b[1;33m@\u001b[0m\u001b[0mproperty\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 130 | "\u001b[1;32mc:\\users\\antreas\\appdata\\local\\programs\\python\\python37\\lib\\json\\__init__.py\u001b[0m in \u001b[0;36mloads\u001b[1;34m(s, encoding, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)\u001b[0m\n\u001b[0;32m 346\u001b[0m \u001b[0mparse_int\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m \u001b[1;32mand\u001b[0m \u001b[0mparse_float\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m \u001b[1;32mand\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 347\u001b[0m parse_constant is None and object_pairs_hook is None and not kw):\n\u001b[1;32m--> 348\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0m_default_decoder\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdecode\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ms\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 349\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mcls\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 350\u001b[0m \u001b[0mcls\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mJSONDecoder\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 131 | "\u001b[1;32mc:\\users\\antreas\\appdata\\local\\programs\\python\\python37\\lib\\json\\decoder.py\u001b[0m in \u001b[0;36mdecode\u001b[1;34m(self, s, _w)\u001b[0m\n\u001b[0;32m 335\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 336\u001b[0m \"\"\"\n\u001b[1;32m--> 337\u001b[1;33m \u001b[0mobj\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mend\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mraw_decode\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ms\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0midx\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0m_w\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ms\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mend\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 338\u001b[0m \u001b[0mend\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0m_w\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ms\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mend\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mend\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 339\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mend\u001b[0m \u001b[1;33m!=\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ms\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 132 | "\u001b[1;32mc:\\users\\antreas\\appdata\\local\\programs\\python\\python37\\lib\\json\\decoder.py\u001b[0m in \u001b[0;36mraw_decode\u001b[1;34m(self, s, idx)\u001b[0m\n\u001b[0;32m 353\u001b[0m \u001b[0mobj\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mend\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mscan_once\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ms\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0midx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 354\u001b[0m \u001b[1;32mexcept\u001b[0m \u001b[0mStopIteration\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0merr\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 355\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mJSONDecodeError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Expecting value\"\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0ms\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0merr\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalue\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mfrom\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 356\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mobj\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mend\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 133 | "\u001b[1;31mJSONDecodeError\u001b[0m: Expecting value: line 1 column 1 (char 0)" 134 | ] 135 | } 136 | ], 137 | "source": [ 138 | "# are you running PMA.start on localhost?\n", 139 | "print(\"Are you running PMA.start? \" + str(core.is_lite()))\n", 140 | "\n", 141 | "# testing actual \"full\" PMA.core instance that's out there\n", 142 | "pma_core_location = \"https://host.pathomation.com/sandbox/2/pma.core\"\n", 143 | "print(\"Are you running PMA.start at \" + pma_core_location + \"? \" + str(core.is_lite(pma_core_location)))\n", 144 | "\n", 145 | "# testing against a non-existing end-point\n", 146 | "pma_core_location = \"https://www.google.com\"\n", 147 | "print(\"Are you running PMA.start at \" + pma_core_location + \"? \" + str(core.is_lite(pma_core_location)))" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "example 20: getting version information about PMA.start" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "# assuming we have PMA.start running; what's the version number?\n", 164 | "print(\"You are running PMA.start version \" + core.get_version_info())" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "version = core.get_api_version()\n", 174 | "pp.pprint(version)" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [ 183 | "core.get_api_verion_string()" 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "metadata": {}, 189 | "source": [ 190 | "example 30: connect to PMA.start" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "sessionID = core.connect() # no parameters needed for PMA.start\n", 200 | "\n", 201 | "if (sessionID == None):\n", 202 | "\tprint(\"Unable to connect to PMA.start\");\n", 203 | "else:\n", 204 | "\tprint(\"Successfully connected to PMA.start; sessionID = \", sessionID)" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "example 40: getting drive letters from PMA.start" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": null, 217 | "metadata": {}, 218 | "outputs": [], 219 | "source": [ 220 | "print(\"You have the following drives in your system: \")\n", 221 | "rootdirs = core.get_root_directories()\n", 222 | "pp.pprint(rootdirs)" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": {}, 228 | "source": [ 229 | "example 60: getting directories PMA.start" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "rootdirs = core.get_root_directories();\n", 239 | "print(\"Directories found in \", rootdirs[0],\":\")\n", 240 | "\n", 241 | "dirs = core.get_directories(rootdirs[0])\n", 242 | "pp.pprint(dirs)" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [ 249 | "example 70: get first non empty directory PMA.start" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "slide_dir = core.get_first_non_empty_directory()\n", 259 | "print (slide_dir)" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "metadata": {}, 265 | "source": [ 266 | "example 80: getting slides PMA.start (recursively and non-recursively)" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": null, 272 | "metadata": {}, 273 | "outputs": [], 274 | "source": [ 275 | "slide_dir = pma_start_slide_dir\n", 276 | "print(\"Looking for slides in \" + slide_dir)\n", 277 | "print()\n", 278 | "\n", 279 | "print (\"**Non-recursive:\")\n", 280 | "print(core.get_slides(slide_dir))\n", 281 | "\n", 282 | "print (\"\\n**One-level deep recursion:\")\n", 283 | "print(core.get_slides(slide_dir, recursive = 1))\n", 284 | "\n", 285 | "print (\"\\n**Full recursion:\")\n", 286 | "print(core.get_slides(slide_dir, recursive = True))\n" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "example 90: get UID for a slide in PMA.start" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": null, 299 | "metadata": {}, 300 | "outputs": [], 301 | "source": [ 302 | "slide_dir = pma_start_slide_dir\n", 303 | "\n", 304 | "print(\"Looking for slides in \" + slide_dir)\n", 305 | "print()\n", 306 | "\n", 307 | "for slide in core.get_slides(slide_dir):\n", 308 | "\tprint (slide,\" - \", core.get_uid(slide))" 309 | ] 310 | }, 311 | { 312 | "cell_type": "markdown", 313 | "metadata": {}, 314 | "source": [ 315 | "example 100: get fingerprint for a slide in PMA.start" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": null, 321 | "metadata": {}, 322 | "outputs": [], 323 | "source": [ 324 | "slide_dir = pma_start_slide_dir\n", 325 | "\n", 326 | "print(\"Looking for slides in \" + slide_dir)\n", 327 | "print()\n", 328 | "\n", 329 | "for slide in core.get_slides(slide_dir):\n", 330 | "\tprint (slide,\" - \", core.get_fingerprint(slide))" 331 | ] 332 | }, 333 | { 334 | "cell_type": "markdown", 335 | "metadata": {}, 336 | "source": [ 337 | "example 110: SlideInfo PMA.start" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": null, 343 | "metadata": {}, 344 | "outputs": [], 345 | "source": [ 346 | "slide_dir = pma_start_slide_dir\n", 347 | "print(\"Looking for slides in\", slide_dir)\n", 348 | "print()\n", 349 | "\n", 350 | "for slide in core.get_slides(slide_dir):\n", 351 | " print(\"***\", slide)\n", 352 | " try:\n", 353 | " pp.pprint(core.get_slide_info(slide))\n", 354 | " except:\n", 355 | " print(\"**Unable to get slide info from this one\")" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "metadata": {}, 361 | "source": [ 362 | "example 115: SlideInfo on an empty file PMA.start" 363 | ] 364 | }, 365 | { 366 | "cell_type": "code", 367 | "execution_count": null, 368 | "metadata": {}, 369 | "outputs": [], 370 | "source": [ 371 | "slide_path = (os.getcwd() + \"\\\\\").replace(\"\\\\\\\\\", \"\\\\\") + \"temp.ndpi\"\n", 372 | "open(slide_path, 'a').close()\n", 373 | "print(\"Created\", slide_path)\n", 374 | "translated_path = slide_path.replace(\"\\\\\", \"/\")\n", 375 | "print(\"Getting info on\", translated_path)\n", 376 | "core.get_slide_info(translated_path)\n", 377 | "os.remove(slide_path)" 378 | ] 379 | }, 380 | { 381 | "cell_type": "markdown", 382 | "metadata": {}, 383 | "source": [ 384 | "example 120: slide dimensions PMA.start" 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": null, 390 | "metadata": {}, 391 | "outputs": [], 392 | "source": [ 393 | "for slide in core.get_slides(pma_start_slide_dir):\n", 394 | " print(\"[\" + slide + \"]\")\n", 395 | " try:\n", 396 | " xdim_pix, ydim_pix = core.get_pixel_dimensions(slide)\n", 397 | " xdim_phys, ydim_phys = core.get_physical_dimensions(slide)\n", 398 | "\n", 399 | " print(\"Pixel dimensions of slide: \", end=\"\")\n", 400 | " print(xdim_pix, \"x\", ydim_pix)\n", 401 | "\n", 402 | " print(\"Slide surface area represented by image: \", end=\"\")\n", 403 | " print(str(xdim_phys) + \"µm x \" + str(ydim_phys) + \"µm = \", end=\"\")\n", 404 | " print(xdim_phys * ydim_phys / 1E6, \" mm2\")\n", 405 | " \n", 406 | " except:\n", 407 | " print(\"**Unable to parse\", slide)" 408 | ] 409 | }, 410 | { 411 | "cell_type": "markdown", 412 | "metadata": {}, 413 | "source": [ 414 | "example 130: get all files that make up a particular slide" 415 | ] 416 | }, 417 | { 418 | "cell_type": "code", 419 | "execution_count": null, 420 | "metadata": {}, 421 | "outputs": [], 422 | "source": [ 423 | "for slide in core.get_slides(pma_start_slide_dir):\n", 424 | " print(slide);\n", 425 | " pp.pprint(core.get_files_for_slide(slide))" 426 | ] 427 | }, 428 | { 429 | "cell_type": "markdown", 430 | "metadata": {}, 431 | "source": [ 432 | "example 140: who are you in PMA.start" 433 | ] 434 | }, 435 | { 436 | "cell_type": "code", 437 | "execution_count": null, 438 | "metadata": {}, 439 | "outputs": [], 440 | "source": [ 441 | "core.who_am_i()" 442 | ] 443 | }, 444 | { 445 | "cell_type": "markdown", 446 | "metadata": {}, 447 | "source": [ 448 | "example 150: investigate zoomlevels PMA.start" 449 | ] 450 | }, 451 | { 452 | "cell_type": "code", 453 | "execution_count": null, 454 | "metadata": {}, 455 | "outputs": [], 456 | "source": [ 457 | "for slide in core.get_slides(pma_start_slide_dir):\n", 458 | " print(\"***\", slide)\n", 459 | " print(\" max zoomlevel:\", core.get_max_zoomlevel(slide))\n", 460 | " print(\" zoomlevel list:\")\n", 461 | " pp.pprint(core.get_zoomlevels_list(slide))\n", 462 | " print(\" zoomlevel dictionary:\")\n", 463 | " pp.pprint(core.get_zoomlevels_dict(slide))" 464 | ] 465 | }, 466 | { 467 | "cell_type": "markdown", 468 | "metadata": {}, 469 | "source": [ 470 | "example 160: investigate magnification characteristics PMA.start" 471 | ] 472 | }, 473 | { 474 | "cell_type": "code", 475 | "execution_count": null, 476 | "metadata": {}, 477 | "outputs": [], 478 | "source": [ 479 | "slide_dir = pma_start_slide_dir\n", 480 | "\n", 481 | "slide_infos = [] # create blank list (to be converted into a pandas DataFrame later)\n", 482 | "\n", 483 | "for slide in core.get_slides(slide_dir):\n", 484 | "\tdict = {\n", 485 | "\t\t\"slide\": core.get_slide_file_name(slide),\n", 486 | "\t\t\"approx_mag\": core.get_magnification(slide, exact=False),\n", 487 | "\t\t\"exact_mag\": core.get_magnification(slide, exact=True),\n", 488 | "\t\t\"is_fluo\": core.is_fluorescent(slide),\n", 489 | "\t\t\"is_zstack\": core.is_z_stack(slide)\n", 490 | "\t\t}\n", 491 | "\tslide_infos.append(dict)\n", 492 | "\t\n", 493 | "df_slides = pd.DataFrame(slide_infos, columns=[\"slide\",\"approx_mag\",\"exact_mag\", \"is_fluo\", \"is_zstack\"])\n", 494 | "print(df_slides) " 495 | ] 496 | }, 497 | { 498 | "cell_type": "markdown", 499 | "metadata": {}, 500 | "source": [ 501 | "example 170: get barcode from slide in PMA.start" 502 | ] 503 | }, 504 | { 505 | "cell_type": "code", 506 | "execution_count": null, 507 | "metadata": {}, 508 | "outputs": [], 509 | "source": [ 510 | "for slide in core.get_slides(pma_start_slide_dir, recursive=True):\n", 511 | " try:\n", 512 | " print(slide, \" - \", core.get_barcode_text(slide))\n", 513 | " except:\n", 514 | " print(\"*Skipping\", slide)" 515 | ] 516 | }, 517 | { 518 | "cell_type": "markdown", 519 | "metadata": {}, 520 | "source": [ 521 | "example 180: show a slide through PMA.start" 522 | ] 523 | }, 524 | { 525 | "cell_type": "code", 526 | "execution_count": null, 527 | "metadata": {}, 528 | "outputs": [], 529 | "source": [ 530 | "slides = core.get_slides(pma_start_slide_dir)\n", 531 | "print(slides[0])\n", 532 | "core.show_slide(slides[0])" 533 | ] 534 | }, 535 | { 536 | "cell_type": "markdown", 537 | "metadata": {}, 538 | "source": [ 539 | "example 190: slide label (URL) in PMA.start" 540 | ] 541 | }, 542 | { 543 | "cell_type": "code", 544 | "execution_count": null, 545 | "metadata": {}, 546 | "outputs": [], 547 | "source": [ 548 | "all_slides = core.get_slides(pma_start_slide_dir)\n", 549 | "for sl in all_slides:\n", 550 | " print(core.get_label_url(sl))\n", 551 | "plt.subplot(1, 2, 1)\n", 552 | "plt.imshow(core.get_label_image(all_slides[0]))\n", 553 | "plt.subplot(1, 2, 2)\n", 554 | "plt.imshow(core.get_label_image(all_slides[1]))" 555 | ] 556 | }, 557 | { 558 | "cell_type": "markdown", 559 | "metadata": {}, 560 | "source": [ 561 | "example 200: slide label (URL) in PMA.start (using barcode alias methods)" 562 | ] 563 | }, 564 | { 565 | "cell_type": "code", 566 | "execution_count": null, 567 | "metadata": {}, 568 | "outputs": [], 569 | "source": [ 570 | "all_slides = core.get_slides(pma_start_slide_dir)\n", 571 | "for sl in all_slides:\n", 572 | " print(core.get_barcode_url(sl))\n", 573 | "plt.subplot(1, 2, 1)\n", 574 | "plt.imshow(core.get_barcode_image(all_slides[0]))\n", 575 | "plt.subplot(1, 2, 2)\n", 576 | "plt.imshow(core.get_barcode_image(all_slides[1]))" 577 | ] 578 | }, 579 | { 580 | "cell_type": "markdown", 581 | "metadata": {}, 582 | "source": [ 583 | "example 210: thumbnail URL and image" 584 | ] 585 | }, 586 | { 587 | "cell_type": "code", 588 | "execution_count": null, 589 | "metadata": {}, 590 | "outputs": [], 591 | "source": [ 592 | "all_slides = core.get_slides(pma_start_slide_dir)\n", 593 | "for sl in all_slides:\n", 594 | " print(core.get_thumbnail_url(sl))\n", 595 | "plt.subplot(1, 2, 1)\n", 596 | "plt.imshow(core.get_thumbnail_image(all_slides[0]))\n", 597 | "plt.subplot(1, 2, 2)\n", 598 | "plt.imshow(core.get_thumbnail_image(all_slides[1]))" 599 | ] 600 | }, 601 | { 602 | "cell_type": "markdown", 603 | "metadata": {}, 604 | "source": [ 605 | "Example 220: retrieving individual tiles in PMA.start" 606 | ] 607 | }, 608 | { 609 | "cell_type": "code", 610 | "execution_count": null, 611 | "metadata": {}, 612 | "outputs": [], 613 | "source": [ 614 | "slides = core.get_slides(pma_start_slide_dir)\n", 615 | "slide = slides[0]\n", 616 | "for zl in range(0, core.get_max_zoomlevel(slide)):\n", 617 | " (x, y, tot) = core.get_number_of_tiles(slide, zl)\n", 618 | " if tot > 16 and x >= 4 and y >= 4:\n", 619 | " break\n", 620 | "for i in range(1,17):\n", 621 | " plt.subplot(4, 4, i)\n", 622 | " xr = 1 + (i-1) % 4\n", 623 | " yr = int((i-1) / 4) + 1\n", 624 | " tile = core.get_tile(slide, xr, yr, zl)\n", 625 | " plt.imshow(tile)" 626 | ] 627 | }, 628 | { 629 | "cell_type": "markdown", 630 | "metadata": {}, 631 | "source": [ 632 | "example 230: searching for slides in PMA.start" 633 | ] 634 | }, 635 | { 636 | "cell_type": "code", 637 | "execution_count": null, 638 | "metadata": {}, 639 | "outputs": [], 640 | "source": [ 641 | "slides = core.search_slides(pma_start_slide_dir, \"mrxs\")\n", 642 | "pp.pprint(slides)" 643 | ] 644 | }, 645 | { 646 | "cell_type": "markdown", 647 | "metadata": {}, 648 | "source": [ 649 | "example 240: search for folders in PMA.start" 650 | ] 651 | }, 652 | { 653 | "cell_type": "code", 654 | "execution_count": null, 655 | "metadata": {}, 656 | "outputs": [], 657 | "source": [ 658 | "slides = core.search_slides(pma_start_slide_dir, \"bladder\")\n", 659 | "pp.pprint(slides)" 660 | ] 661 | } 662 | ], 663 | "metadata": { 664 | "kernelspec": { 665 | "display_name": "Python 3", 666 | "language": "python", 667 | "name": "python3" 668 | }, 669 | "language_info": { 670 | "codemirror_mode": { 671 | "name": "ipython", 672 | "version": 3 673 | }, 674 | "file_extension": ".py", 675 | "mimetype": "text/x-python", 676 | "name": "python", 677 | "nbconvert_exporter": "python", 678 | "pygments_lexer": "ipython3", 679 | "version": "3.7.2" 680 | } 681 | }, 682 | "nbformat": 4, 683 | "nbformat_minor": 2 684 | } 685 | -------------------------------------------------------------------------------- /samples/jupyter/PMA.view.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Setup" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Use pip to download and install the necessary libraries if needed" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "!pip install --upgrade pma_python" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "Import libraries and set connection parameters" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 1, 36 | "metadata": {}, 37 | "outputs": [ 38 | { 39 | "name": "stdout", 40 | "output_type": "stream", 41 | "text": [ 42 | "pma_python library loaded; version 2.0.0.113\n" 43 | ] 44 | } 45 | ], 46 | "source": [ 47 | "# helper libraries\n", 48 | "\n", 49 | "# pma_python\n", 50 | "from pma_python import view\n", 51 | "print(\"pma_python library loaded; version\", view.__version__)\n", 52 | "\n", 53 | "# connection parameters to be used throughout this notebook\n", 54 | "pma_view_server = \"http://host.pathomation.com/sandbox/2/PMA.view\"" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 2, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "# only needed when debugging code for extra error messages:\n", 64 | "view.set_debug_flag(False)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "# PMA.view examples" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "example 2020: getting version information about PMA.view" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [ 86 | { 87 | "name": "stdout", 88 | "output_type": "stream", 89 | "text": [ 90 | "Investigating http://host.pathomation.com/sandbox/2/PMA.view\n", 91 | "You are running PMA.view version 2.0.0.788\n" 92 | ] 93 | } 94 | ], 95 | "source": [ 96 | "# assuming we have PMA.view running; what's the version number?\n", 97 | "print(\"Investigating\", pma_view_server)\n", 98 | "print(\"You are running PMA.view version\", view.get_version_info(pma_view_server))\n", 99 | "\n", 100 | "#ERROR: the following statement should return None instead of crashing\n", 101 | "version = view.get_version_info(\"http://nowhere\");\n", 102 | "print (version)\n", 103 | "if (version == None):\n", 104 | "\tprint(\"Unable to detect PMA.view at specified location (http://nowhere/)\")\n", 105 | "else:\n", 106 | "\tprint(\"You are running PMA.view version\", version);" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [] 115 | } 116 | ], 117 | "metadata": { 118 | "file_extension": ".py", 119 | "kernelspec": { 120 | "display_name": "Python 3", 121 | "language": "python", 122 | "name": "python3" 123 | }, 124 | "language_info": { 125 | "codemirror_mode": { 126 | "name": "ipython", 127 | "version": 3 128 | }, 129 | "file_extension": ".py", 130 | "mimetype": "text/x-python", 131 | "name": "python", 132 | "nbconvert_exporter": "python", 133 | "pygments_lexer": "ipython3", 134 | "version": "3.7.2" 135 | }, 136 | "mimetype": "text/x-python", 137 | "name": "python", 138 | "npconvert_exporter": "python", 139 | "pygments_lexer": "ipython3", 140 | "version": 3 141 | }, 142 | "nbformat": 4, 143 | "nbformat_minor": 2 144 | } 145 | -------------------------------------------------------------------------------- /samples/test.py: -------------------------------------------------------------------------------- 1 | import os, warnings 2 | warnings.simplefilter('ignore') 3 | from pma_python import * 4 | 5 | 6 | def scenario1(url, username, password, verify): 7 | print("Session ID: ", connect(url, username, password, verify)) 8 | 9 | 10 | def scenario2(url): 11 | print(f'PMA.core.lite: {is_lite(url)}') 12 | 13 | 14 | def scenario3(url, verify): 15 | print(f'\npmaviewURL version: {get_version_info(url)}') 16 | print(f'PMA.core API version: {get_api_version(url, verify)}') 17 | print(f'Built revision: {get_build_revision(url, verify)}') 18 | 19 | 20 | def scenario4(parent_dir, sub_dir, session_id, recursive, verify): 21 | print(f'\nRoot directories: {get_root_directories(session_id, verify)}\n') 22 | print(f'Sub-directories: {get_directories(parent_dir, session_id, recursive, verify)}\n') 23 | print(f'Available slides: {get_slides(sub_dir, session_id, recursive, verify)}') 24 | 25 | 26 | def scenario5(slide, session_id, verify): 27 | print(f'\nSlide "{os.path.basename(os.path.normpath(slide))}" info: {get_slide_info(slide, session_id, verify)}\n') 28 | print(f'Slide "{os.path.basename(os.path.normpath(slide))}" UID: {get_uid(slide, session_id, verify)}\n') 29 | print(f'Slide "{os.path.basename(os.path.normpath(slide))}" fingerprint: {get_fingerprint(slide, session_id, verify)}\n') 30 | print(f'Slide "{os.path.basename(os.path.normpath(slide))}" barcode: {get_barcode_text(slide, session_id, verify)}') 31 | 32 | 33 | def main(): 34 | try: 35 | url = str(input("\nEnter the PMA.core URL: ")) 36 | username = str(input("\nUsername: ")) 37 | password = str(input("\nPassword: ")) 38 | 39 | scenario_num = int(input("\nEnter the scenario number: ")) 40 | 41 | if scenario_num == 1: 42 | # 1. Connect to PMA.core instance 43 | scenario1(url=url, username=username, password=password, verify=False) 44 | elif scenario_num == 2: 45 | # 2. Check if PMA.core.lite (server component of PMA.start) 46 | scenario2(url=url) 47 | elif scenario_num == 3: 48 | # 3a. Check the 'pmaviewURL' version 49 | # 3b. Check the API version exposed by the underlying PMA.core 50 | # 3c. Get build revision from PMA.core instance running at pmacoreURL 51 | scenario3(url=url, verify=False) 52 | elif scenario_num == 4: 53 | # 4a. Return an array of root-directories available to sessionID 54 | # 4b. Return an array of sub-directories available to sessionID in the startDir directory 55 | # 4c. Return an array of slides available to sessionID in the startDir directory 56 | parent_dir = str(input("\nEnter the parent directory: ")) 57 | sub_dir = str(input("\nEnter the sub-directory: ")) 58 | scenario4(parent_dir=parent_dir, sub_dir=sub_dir, 59 | session_id=connect(url, username, password, False), 60 | recursive=False, verify=False) 61 | elif scenario_num == 5: 62 | # 5a. Return raw image information in the form of nested dictionaries 63 | # 5b. Get the UID for a specific slide 64 | # 5c. Get the fingerprint for a specific slide 65 | # 5d. Get the text encoded by the barcode (if there IS a barcode on the slide to begin with) 66 | sub_dir = str(input("\nEnter the sub-directory: ")) 67 | selected_slide = get_slides(sub_dir, connect(url, username, password, False), False, False)[0] 68 | scenario5(slide=selected_slide, session_id=connect(url, username, password, False), verify=False) 69 | else: 70 | raise ValueError("Invalid input. Please enter a value [1-5]") 71 | except Exception as e: 72 | print(f'Error: {str(e)}') 73 | print(f'URL or wrong information (credentials, directories)') 74 | 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | def read(file_name): 5 | with open(os.path.join(os.path.dirname(__file__), file_name)) as f: 6 | return f.read() 7 | 8 | def extract_version(v): 9 | idx1 = v.find("'") 10 | idx2 = v.rfind("'") 11 | id = "" 12 | if (idx1 > 0 and idx2 > 0): 13 | id = v[(idx1+1):] 14 | l = idx2 - idx1 15 | id = id[0:l-1] 16 | else: 17 | idx1 = v.find('"') 18 | idx2 = v.rfind('"') 19 | id = v[(idx1+1):] 20 | l = idx2 - idx1 21 | id = id[0:l-1] 22 | return id 23 | 24 | setup(name='pma_python', 25 | version=extract_version(read("pma_python/version.py")), 26 | description='Universal viewing of digital microscopy, whole slide imaging and digital pathology data', 27 | long_description=read('long_desc.txt'), 28 | url='http://github.com/pathomation/pma_python', 29 | author='Pathomation', 30 | author_email='info@pathomation.com', 31 | license='http://free.pathomation.com/eula/', 32 | packages=['pma_python'], 33 | data_files=[('', ['long_desc.txt'])], 34 | classifiers=[ 35 | 'Development Status :: 3 - Alpha', 36 | 'Programming Language :: Python :: 3'], 37 | keywords='wsi whole slide imaging gigapixel microscopy histology pathology', 38 | install_requires=['pandas', 'pillow', 'requests', 'requests_toolbelt'], 39 | python_requires='>=3', # assume this only works in Python 3 40 | zip_safe=False) 41 | 42 | --------------------------------------------------------------------------------