aignostics 0.2.22__tar.gz → 0.2.27__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {aignostics-0.2.22 → aignostics-0.2.27}/PKG-INFO +1 -1
  2. {aignostics-0.2.22 → aignostics-0.2.27}/examples/notebook.py +1 -1
  3. {aignostics-0.2.22 → aignostics-0.2.27}/pyproject.toml +2 -2
  4. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/_page_application_run_describe.py +43 -18
  5. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/notebook/_gui.py +23 -6
  6. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/notebook/_notebook.py +7 -30
  7. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/wsi/_gui.py +14 -19
  8. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/wsi/_openslide_handler.py +16 -1
  9. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/wsi/_service.py +46 -63
  10. aignostics-0.2.27/src/aignostics/wsi/assets/fallback.png +0 -0
  11. {aignostics-0.2.22 → aignostics-0.2.27}/.gitignore +0 -0
  12. {aignostics-0.2.22 → aignostics-0.2.27}/LICENSE +0 -0
  13. {aignostics-0.2.22 → aignostics-0.2.27}/README.md +0 -0
  14. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/.openapi-generator/FILES +0 -0
  15. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/.openapi-generator/VERSION +0 -0
  16. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/api/public_api.py +0 -0
  17. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/api_client.py +0 -0
  18. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/api_response.py +0 -0
  19. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/configuration.py +0 -0
  20. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/exceptions.py +0 -0
  21. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/__init__.py +0 -0
  22. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/application_read_response.py +0 -0
  23. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/application_run_status.py +0 -0
  24. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/application_version_read_response.py +0 -0
  25. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/http_validation_error.py +0 -0
  26. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/input_artifact_creation_request.py +0 -0
  27. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/input_artifact_read_response.py +0 -0
  28. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/item_creation_request.py +0 -0
  29. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/item_result_read_response.py +0 -0
  30. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/item_status.py +0 -0
  31. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/output_artifact_read_response.py +0 -0
  32. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/output_artifact_result_read_response.py +0 -0
  33. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/output_artifact_scope.py +0 -0
  34. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/payload_input_artifact.py +0 -0
  35. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/payload_item.py +0 -0
  36. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/payload_output_artifact.py +0 -0
  37. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/run_creation_request.py +0 -0
  38. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/run_creation_response.py +0 -0
  39. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/run_read_response.py +0 -0
  40. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/transfer_urls.py +0 -0
  41. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/user_payload.py +0 -0
  42. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/validation_error.py +0 -0
  43. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/models/validation_error_loc_inner.py +0 -0
  44. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/aignx/codegen/rest.py +0 -0
  45. {aignostics-0.2.22 → aignostics-0.2.27}/codegen/out/docs/PublicApi.md +0 -0
  46. {aignostics-0.2.22 → aignostics-0.2.27}/examples/__init__.py +0 -0
  47. {aignostics-0.2.22 → aignostics-0.2.27}/examples/e2e.py +0 -0
  48. {aignostics-0.2.22 → aignostics-0.2.27}/examples/notebook.ipynb +0 -0
  49. {aignostics-0.2.22 → aignostics-0.2.27}/examples/script.py +0 -0
  50. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/__init__.py +0 -0
  51. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/__init__.py +0 -0
  52. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_cli.py +0 -0
  53. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/_frame.py +0 -0
  54. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/_page_application_describe.py +0 -0
  55. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/_page_builder.py +0 -0
  56. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/_page_index.py +0 -0
  57. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/_utils.py +0 -0
  58. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/canceled.lottie +0 -0
  59. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/done.lottie +0 -0
  60. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/empty.lottie +0 -0
  61. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/error.lottie +0 -0
  62. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/error_alt.lottie +0 -0
  63. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/home-card-1.png +0 -0
  64. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/home-card-2.png +0 -0
  65. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/home-card-3.png +0 -0
  66. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/home-card-4.png +0 -0
  67. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/home-card-5.png +0 -0
  68. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/image-not-found.png +0 -0
  69. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/pending.lottie +0 -0
  70. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/pending_alt.lottie +0 -0
  71. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/ruo.png +0 -0
  72. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_gui/assets/ruo.svg +0 -0
  73. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_service.py +0 -0
  74. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_settings.py +0 -0
  75. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/application/_utils.py +0 -0
  76. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/bucket/__init__.py +0 -0
  77. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/bucket/_cli.py +0 -0
  78. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/bucket/_gui.py +0 -0
  79. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/bucket/_service.py +0 -0
  80. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/bucket/_settings.py +0 -0
  81. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/bucket/assets/Google-Cloud-logo.png +0 -0
  82. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/bucket/assets/Google-Cloud-logo.svg +0 -0
  83. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/cli.py +0 -0
  84. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/constants.py +0 -0
  85. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/dataset/__init__.py +0 -0
  86. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/dataset/_cli.py +0 -0
  87. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/dataset/_gui.py +0 -0
  88. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/dataset/_idc_index.py +0 -0
  89. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/dataset/_service.py +0 -0
  90. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/dataset/assets/NIH-IDC-logo.svg +0 -0
  91. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/gui/__init__.py +0 -0
  92. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/gui/_frame.py +0 -0
  93. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/gui/_theme.py +0 -0
  94. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/gui/assets/cabin-v27-latin-regular.woff2 +0 -0
  95. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/gui/assets/logo.png +0 -0
  96. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/notebook/__init__.py +0 -0
  97. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/notebook/_service.py +0 -0
  98. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/notebook/assets/python.lottie +0 -0
  99. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/__init__.py +0 -0
  100. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/_authentication.py +0 -0
  101. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/_client.py +0 -0
  102. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/_constants.py +0 -0
  103. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/_messages.py +0 -0
  104. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/_service.py +0 -0
  105. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/_settings.py +0 -0
  106. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/_utils.py +0 -0
  107. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/resources/__init__.py +0 -0
  108. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/resources/applications.py +0 -0
  109. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/resources/runs.py +0 -0
  110. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/platform/resources/utils.py +0 -0
  111. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/qupath/__init__.py +0 -0
  112. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/qupath/_cli.py +0 -0
  113. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/qupath/_gui.py +0 -0
  114. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/qupath/_service.py +0 -0
  115. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/qupath/_settings.py +0 -0
  116. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/qupath/assets/download.lottie +0 -0
  117. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/qupath/assets/microscope.lottie +0 -0
  118. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/system/__init__.py +0 -0
  119. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/system/_cli.py +0 -0
  120. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/system/_exceptions.py +0 -0
  121. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/system/_gui.py +0 -0
  122. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/system/_service.py +0 -0
  123. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/system/_settings.py +0 -0
  124. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/.vendored/bottle.py +0 -0
  125. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/__init__.py +0 -0
  126. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_cli.py +0 -0
  127. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_console.py +0 -0
  128. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_constants.py +0 -0
  129. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_di.py +0 -0
  130. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_gui.py +0 -0
  131. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_health.py +0 -0
  132. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_log.py +0 -0
  133. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_logfire.py +0 -0
  134. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_notebook.py +0 -0
  135. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_process.py +0 -0
  136. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_sentry.py +0 -0
  137. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_service.py +0 -0
  138. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/_settings.py +0 -0
  139. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/utils/boot.py +0 -0
  140. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/wsi/__init__.py +0 -0
  141. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/wsi/_cli.py +0 -0
  142. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/wsi/_pydicom_handler.py +0 -0
  143. {aignostics-0.2.22 → aignostics-0.2.27}/src/aignostics/wsi/_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aignostics
3
- Version: 0.2.22
3
+ Version: 0.2.27
4
4
  Summary: 🔬 Python SDK providing access to the Aignostics Platform. Includes Aignostics Launchpad (Desktop Application), Aignostics CLI (Command-Line Interface), example notebooks, and Aignostics Client Library.
5
5
  Project-URL: Homepage, https://aignostics.readthedocs.io/en/latest/
6
6
  Project-URL: Documentation, https://aignostics.readthedocs.io/en/latest/
@@ -2,7 +2,7 @@
2
2
  # requires-python = ">=3.13"
3
3
  # dependencies = [
4
4
  # "marimo",
5
- # "aignostics==0.2.22",
5
+ # "aignostics==0.2.27",
6
6
  # ]
7
7
  # ///
8
8
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aignostics"
3
- version = "0.2.22"
3
+ version = "0.2.27"
4
4
  description = "🔬 Python SDK providing access to the Aignostics Platform. Includes Aignostics Launchpad (Desktop Application), Aignostics CLI (Command-Line Interface), example notebooks, and Aignostics Client Library."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -355,7 +355,7 @@ source = ["src/"]
355
355
 
356
356
 
357
357
  [tool.bumpversion]
358
- current_version = "0.2.22"
358
+ current_version = "0.2.27"
359
359
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
360
360
  serialize = ["{major}.{minor}.{patch}"]
361
361
  search = "{current_version}"
@@ -4,7 +4,7 @@ from importlib.util import find_spec
4
4
  from multiprocessing import Manager
5
5
  from pathlib import Path
6
6
  from typing import Any
7
- from urllib.parse import quote as urlencode
7
+ from urllib.parse import quote
8
8
 
9
9
  from nicegui import run as nicegui_run
10
10
  from nicegui import ui # noq
@@ -87,7 +87,7 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
87
87
  return False
88
88
 
89
89
  @ui.refreshable
90
- def download_run_dialog_content(qupath_project: bool = False) -> None: # noqa: C901, PLR0915
90
+ def download_run_dialog_content(qupath_project: bool = False, marimo: bool = False) -> None: # noqa: C901, PLR0915
91
91
  if qupath_project:
92
92
  ui.markdown(
93
93
  "##### Visualize results in QuPath with one click \n"
@@ -97,6 +97,13 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
97
97
  "4. The QuPath project will reference the input slides and resulting image heatmaps. \n"
98
98
  "5. Detected cells will be added as annotations to input slides. \n"
99
99
  )
100
+ elif marimo:
101
+ ui.markdown(
102
+ "##### Open results in Marimo with one click \n"
103
+ "1. Use data directory of Launchpad or select a custom folder. \n"
104
+ "2. A subfolder with the application run will be created and all results downloaded there. \n"
105
+ "3. A Marimo notebook will be started pointing to the subfolder. \n"
106
+ )
100
107
  else:
101
108
  ui.markdown(
102
109
  "##### Download all results with one click \n"
@@ -223,6 +230,10 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
223
230
  ui.notify("Download and QuPath project creation completed.", type="positive")
224
231
  download_item_status.set_text("Opening QuPath ...")
225
232
  await open_qupath(project=results_folder / "qupath", button=download_button)
233
+ elif marimo:
234
+ ui.notify("Download and Notebook preparation completed.", type="positive")
235
+ download_item_status.set_text("Opening Notebook ...")
236
+ open_marimo(results_folder=results_folder, button=download_button)
226
237
  else:
227
238
  ui.notify("Download completed.", type="positive")
228
239
  show_in_file_manager(str(results_folder))
@@ -238,10 +249,20 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
238
249
 
239
250
  ui.separator()
240
251
  with ui.row(align_items="end").classes("w-full justify-end"):
252
+ if qupath_project:
253
+ label = "Visualize with QuPath"
254
+ icon = "zoom_in"
255
+ elif marimo:
256
+ label = "Analyze with Notebook"
257
+ icon = "analytics"
258
+ else:
259
+ label = "Download all results"
260
+ icon = "cloud_download"
261
+
241
262
  download_button = (
242
263
  ui.button(
243
- "Download all results" if not qupath_project else "Visualize with QuPath",
244
- icon="cloud_download" if not qupath_project else "zoom_in",
264
+ label,
265
+ icon=icon,
245
266
  on_click=start_download,
246
267
  )
247
268
  .props("color=primary")
@@ -254,9 +275,9 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
254
275
  with ui.dialog() as download_run_dialog, ui.card().style(WIDTH_1200px):
255
276
  download_run_dialog_content()
256
277
 
257
- def download_run_dialog_open(qupath_project: bool = False) -> None:
278
+ def download_run_dialog_open(qupath_project: bool = False, marimo: bool = False) -> None:
258
279
  """Open the CSV dialog."""
259
- download_run_dialog_content.refresh(qupath_project=qupath_project)
280
+ download_run_dialog_content.refresh(qupath_project=qupath_project, marimo=marimo)
260
281
  download_run_dialog.open()
261
282
 
262
283
  @ui.refreshable
@@ -315,7 +336,7 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
315
336
  if url:
316
337
  try:
317
338
  with ui.scroll_area().classes("w-full h-[calc(100vh-2rem)]"):
318
- ui.image("/tiff?url=" + urlencode(url))
339
+ ui.image("/tiff?url=" + quote(url))
319
340
  except Exception as e: # noqa: BLE001
320
341
  ui.notify(f"Failed to load CSV: {e!s}", type="negative")
321
342
 
@@ -361,11 +382,11 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
361
382
  button.enable()
362
383
  button.props(remove="loading")
363
384
 
364
- def open_marimo() -> None:
365
- marimo_button.disable()
366
- marimo_button.props(add="loading")
367
- ui.navigate.to(f"/notebook/{run.application_run_id}")
368
- marimo_button.enable()
385
+ def open_marimo(results_folder: Path, button: ui.button | None = None) -> None:
386
+ if button:
387
+ button.disable()
388
+ button.props(add="loading")
389
+ ui.navigate.to(f"/notebook/{run.application_run_id}?results_folder={quote(results_folder.as_posix())}")
369
390
 
370
391
  if run_data: # noqa: PLR1702
371
392
  with ui.row().classes("w-full justify-center"):
@@ -399,11 +420,15 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
399
420
  ):
400
421
  ui.tooltip("Open results in QuPath Microscopy Viewer")
401
422
  if find_spec("marimo"):
402
- with ui.button(
403
- "Marimo",
404
- icon="analytics",
405
- on_click=open_marimo,
406
- ).props("push") as marimo_button:
423
+ with (
424
+ ui.button(
425
+ "Marimo",
426
+ icon="analytics",
427
+ on_click=lambda _: download_run_dialog_open(qupath_project=False, marimo=True),
428
+ )
429
+ .mark("BUTTON_OPEN_NOTEBOOK")
430
+ .props("push")
431
+ ):
407
432
  ui.tooltip("Open results in Python Notebook served by Marimo")
408
433
  if run_data.status.value == ApplicationRunStatus.RUNNING:
409
434
  ui.button(
@@ -434,7 +459,7 @@ async def _page_application_run_describe(application_run_id: str) -> None: # no
434
459
  ):
435
460
  image_file: Path | None = Path(item.reference).resolve()
436
461
  if image_file and image_file.is_file():
437
- image_url = "/thumbnail?source=" + urlencode(image_file.as_posix())
462
+ image_url = "/thumbnail?source=" + quote(image_file.as_posix())
438
463
  else:
439
464
  image_file = None
440
465
  image_url = "/application_assets/image-not-found.png"
@@ -1,8 +1,10 @@
1
1
  """Notebook GUI."""
2
2
 
3
3
  from pathlib import Path
4
+ from urllib.parse import quote
4
5
 
5
6
  from aignostics.gui import frame, theme
7
+ from aignostics.system import Service as SystemService
6
8
  from aignostics.utils import BasePageBuilder, get_logger
7
9
 
8
10
  logger = get_logger(__name__)
@@ -24,6 +26,14 @@ class PageBuilder(BasePageBuilder):
24
26
  # Nothing to do here, just to show the page
25
27
  pass
26
28
 
29
+ def launch_marimo() -> None:
30
+ """Launch QuPath."""
31
+ launch_button.props(add="loading")
32
+ ui.notify("Launching Python Notebook...", color="blue")
33
+ ui.navigate.to(
34
+ f"/notebook/all?results_folder={quote(SystemService.get_user_data_directory('results').as_posix())}"
35
+ )
36
+
27
37
  ui.markdown(
28
38
  """
29
39
  ### Manage your Marimo Extension
@@ -36,7 +46,12 @@ class PageBuilder(BasePageBuilder):
36
46
  "Marimo is installed and ready to execute. "
37
47
  "Go to a completed application result and click the Notebook button."
38
48
  )
39
-
49
+ with ui.row().classes("w-full justify-between items-center"):
50
+ launch_button = ui.button(
51
+ "Open",
52
+ on_click=launch_marimo,
53
+ icon="visibility",
54
+ ).mark("BUTTON_NOTEBOOK_LAUNCH")
40
55
  ui.markdown(
41
56
  """
42
57
  ###### What is Marimo?
@@ -73,23 +88,25 @@ class PageBuilder(BasePageBuilder):
73
88
  ui.space()
74
89
 
75
90
  @ui.page("/notebook/{application_run_id}")
76
- def page_application_run_marimo(application_run_id: str) -> None:
91
+ def page_application_run_marimo(application_run_id: str, results_folder: str) -> None:
77
92
  """Inspect Application Run in Marimo."""
78
93
  theme()
79
94
 
80
95
  with ui.row().classes("w-full justify-end"):
81
96
  ui.button(
82
- "Back to Application Run",
97
+ "Back to Application Run" if application_run_id != "all" else "Back to Marimo Extension",
83
98
  icon="arrow_back",
84
99
  on_click=lambda _, application_run_id=application_run_id: ui.navigate.to(
85
- f"/application/run/{application_run_id}"
100
+ f"/application/run/{application_run_id}" if application_run_id != "all" else "/notebook"
86
101
  ),
87
- )
102
+ ).mark("BUTTON_NOTEBOOK_BACK")
88
103
 
89
104
  try:
90
105
  server_url = Service().start()
91
106
  ui.html(
92
- f'<iframe src="{server_url}?run_id={application_run_id}" width="100%" height="100%"></iframe>'
107
+ f'<iframe src="{server_url}?application_run_id={application_run_id}'
108
+ f'&results_folder={quote(results_folder)}" '
109
+ 'width="100%" height="100%"></iframe>'
93
110
  ).classes("w-full h-[calc(100vh-5rem)]")
94
111
  except Exception:
95
112
  message = "Failed to start Marimo server."
@@ -8,22 +8,13 @@
8
8
 
9
9
  import marimo
10
10
 
11
- __generated_with = "0.13.10"
11
+ __generated_with = "0.13.15"
12
12
  app = marimo.App(width="full")
13
13
 
14
14
 
15
- @app.cell
16
- def _(mo):
17
- query_params = mo.query_params()
18
- print(query_params)
19
-
20
- print("Hallo Test Omid")
21
- return
22
-
23
-
24
15
  @app.cell
25
16
  def _():
26
- """E2E Demo using DICOM data from National Cancer Institute
17
+ """Aignostics Notebook
27
18
 
28
19
  Notes:
29
20
  - See https://colab.research.google.com/github/ImagingDataCommons/IDC-Tutorials/blob/master/notebooks/pathomics/microscopy_dicom_ann_intro.ipynb#scrollTo=fMRsnFlzinO5
@@ -39,14 +30,6 @@ def _():
39
30
  from aignostics import dataset
40
31
  from wsidicom import WsiDicom
41
32
 
42
- load_dotenv()
43
-
44
- os.environ
45
-
46
- print(os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "default"))
47
-
48
- print(Path.cwd())
49
-
50
33
  mo.sidebar([
51
34
  mo.md("# aignostics"),
52
35
  mo.nav_menu(
@@ -61,23 +44,17 @@ def _():
61
44
  orientation="vertical",
62
45
  ),
63
46
  ])
64
-
65
- mo.vstack([
66
- mo.ui.file_browser( # type: ignore
67
- GSPath("gs://aignx-storage-service-dev/sample_data_formatted"), # type: ignore
68
- filetypes=[".dcm", ".tif", ".tiff", ".svs", ".json"],
69
- multiple=True,
70
- restrict_navigation=True,
71
- )
72
- ]) # type: ignore
73
47
  return Path, WsiDicom, dataset, mo, plt
74
- ex
48
+
75
49
 
76
50
  @app.cell
77
51
  def _(Path, mo):
52
+ query_params = mo.query_params()
53
+ application_run_id = query_params["application_run_id"]
54
+ print(f"Application run id '{application_run_id}'")
78
55
  mo.vstack([
79
56
  mo.ui.file_browser(
80
- Path("tmp"),
57
+ Path(query_params["results_folder"]),
81
58
  filetypes=[".dcm", ".tif", ".tiff", ".svs"],
82
59
  multiple=True,
83
60
  restrict_navigation=True,
@@ -2,7 +2,8 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- from fastapi import HTTPException, Response
5
+ from fastapi import Response
6
+ from fastapi.responses import RedirectResponse
6
7
 
7
8
  from aignostics.utils import BasePageBuilder, get_logger
8
9
 
@@ -16,6 +17,8 @@ class PageBuilder(BasePageBuilder):
16
17
  def register_pages() -> None:
17
18
  from nicegui import app # noqa: PLC0415
18
19
 
20
+ app.add_static_files("/wsi_assets", Path(__file__).parent / "assets")
21
+
19
22
  @app.get("/thumbnail")
20
23
  def thumbnail(source: str) -> Response:
21
24
  """Serve a thumbnail for a given source reference.
@@ -24,21 +27,16 @@ class PageBuilder(BasePageBuilder):
24
27
  source (str): The source of the slide pointing to a file on the filesystem.
25
28
 
26
29
  Returns:
27
- fastapi.Response: HTTP response containing the thumbnail image.
28
-
29
- Raises:
30
- HTTPException: If the file does not exist or if thumbnail generation fails.
30
+ fastapi.Response: HTTP response containing the thumbnail or fallback image.
31
31
  """
32
32
  try:
33
33
  return Response(content=Service().get_thumbnail_bytes(Path(source)), media_type="image/png")
34
- except ValueError as e:
34
+ except ValueError:
35
35
  logger.warning("Error generating thumbnail on bad request or invalid image input")
36
- raise HTTPException(status_code=400, detail=f"Bad request or invalid image input: {e!s}") from e
37
- except RuntimeError as e:
36
+ return RedirectResponse("/wsi_assets/fallback.png")
37
+ except RuntimeError:
38
38
  logger.exception("Internal server error when generating thumbnail")
39
- raise HTTPException(
40
- status_code=500, detail=f"Internal server error when generating thumbnail: {e!s}"
41
- ) from e
39
+ return RedirectResponse("/wsi_assets/fallback.png")
42
40
 
43
41
  @app.get("/tiff")
44
42
  def tiff(url: str) -> Response:
@@ -48,16 +46,13 @@ class PageBuilder(BasePageBuilder):
48
46
  url (str): The URL of the tiff.
49
47
 
50
48
  Returns:
51
- fastapi.Response: HTTP response containing the thumbnail image.
52
-
53
- Raises:
54
- HTTPException: If the file does not exist or if thumbnail generation fails.
49
+ fastapi.Response: HTTP response containing the converted tiff or fallback image
55
50
  """
56
51
  try:
57
52
  return Response(content=Service().get_tiff_as_jpg(url), media_type="image/jpeg")
58
- except ValueError as e:
53
+ except ValueError:
59
54
  logger.warning("Error generating jpeg on bad request or invalid tiff input")
60
- raise HTTPException(status_code=400, detail=f"Bad request or invalid tiff input: {e!s}") from e
61
- except RuntimeError as e:
55
+ return RedirectResponse("/wsi_assets/fallback.png")
56
+ except RuntimeError:
62
57
  logger.exception("Internal server error when generating jpeg")
63
- raise HTTPException(status_code=500, detail=f"Internal server error when generating jpeg: {e!s}") from e
58
+ return RedirectResponse("/wsi_assets/fallback.png")
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from typing import Any
6
6
 
7
7
  import openslide
8
+ from openslide import ImageSlide, OpenSlide, open_slide
8
9
  from PIL.Image import Image
9
10
 
10
11
  TIFF_IMAGE_DESCRIPTION = "tiff.ImageDescription"
@@ -15,10 +16,21 @@ class OpenSlideHandler:
15
16
 
16
17
  def __init__(self, path: str) -> None:
17
18
  self.path = Path(path)
18
- self.slide = openslide.OpenSlide(str(path))
19
+ self.slide: OpenSlide | ImageSlide = open_slide(str(path))
19
20
 
20
21
  @classmethod
21
22
  def from_file(cls, path: str | Path) -> "OpenSlideHandler":
23
+ """Create an OpenSlideHandler from a file path.
24
+
25
+ Args:
26
+ path (str | Path): The path to the WSI file.
27
+
28
+ Returns:
29
+ OpenSlideHandler: An instance of OpenSlideHandler initialized with the given path.
30
+
31
+ Raises:
32
+ OpenSlideError: If the file cannot be opened as a WSI.
33
+ """
22
34
  return cls(str(path))
23
35
 
24
36
  def _detect_format(self) -> str | None:
@@ -55,6 +67,9 @@ class OpenSlideHandler:
55
67
 
56
68
  Returns:
57
69
  Image: Thumbnail image of the slide.
70
+
71
+ Raises:
72
+ OpenSlideError: If the slide cannot be opened or if thumbnail generation fails.
58
73
  """
59
74
  return self.slide.get_thumbnail((256, 256))
60
75
 
@@ -12,6 +12,8 @@ logger = get_logger(__name__)
12
12
 
13
13
  TIMEOUT = 60 # 1 minutes
14
14
 
15
+ FILE_TYPES_SUPPORTED = {".dcm", ".tiff", ".tif", ".svs"}
16
+
15
17
 
16
18
  class Service(BaseService):
17
19
  """Service of the application module."""
@@ -34,58 +36,8 @@ class Service(BaseService):
34
36
  status=Health.Code.UP,
35
37
  )
36
38
 
37
- def _get_openslide_metadata(self, path: Path) -> dict[str, Any]: # noqa: PLR6301
38
- """Get metadata of a wsi file via OpenSlide.
39
-
40
- Args:
41
- path (Path): Path to the wsi file.
42
-
43
- Returns:
44
- dict[str, Any]: Metadata of the wsi file.
45
- """
46
- from ._openslide_handler import OpenSlideHandler # noqa: PLC0415
47
-
48
- handler = OpenSlideHandler.from_file(path)
49
- return handler.get_metadata()
50
-
51
- def _get_openslide_thumbnail(self, path: Path) -> "PIL.Image.Image": # type: ignore # noqa: F821, PLR6301
52
- """Get thumbnail of a wsi file via OpenSlide.
53
-
54
- Args:
55
- path (Path): Path to the wsi file.
56
-
57
- Returns:
58
- PIL.Image.Image: Thumbnail of the wsi file.
59
-
60
- Raises:
61
- OpenSlideError: If there is an error processing the wsi file with OpenSlide.
62
- """
63
- from openslide import OpenSlideError, OpenSlideUnsupportedFormatError # noqa: PLC0415
64
- from PIL import Image as PILImage # noqa: PLC0415
65
-
66
- from ._openslide_handler import OpenSlideHandler # noqa: PLC0415
67
-
68
- try:
69
- handler = OpenSlideHandler.from_file(path)
70
- return handler.get_thumbnail()
71
- except OpenSlideUnsupportedFormatError:
72
- # If OpenSlide fails, try using PIL directly
73
- img_file = PILImage.open(path)
74
- # Create a thumbnail with max size 256x256 while maintaining aspect ratio
75
- img_file.thumbnail((256, 256))
76
- # Convert to RGB mode if needed (for PNG compatibility)
77
- return img_file.convert("RGB") if img_file.mode not in {"RGB", "RGBA"} else img_file.copy()
78
- except OpenSlideError as e:
79
- if str(e) == "No pyramid levels found":
80
- # If regular OpenSlide fails, try using PIL directly
81
- img_file = PILImage.open(path)
82
- # Create a thumbnail with max size 256x256 while maintaining aspect ratio
83
- img_file.thumbnail((256, 256))
84
- # Convert to RGB mode if needed (for PNG compatibility)
85
- return img_file.convert("RGB") if img_file.mode not in {"RGB", "RGBA"} else img_file.copy()
86
- raise
87
-
88
- def get_thumbnail(self, path: Path) -> "PIL.Image.Image": # type: ignore # noqa: F821
39
+ @staticmethod
40
+ def get_thumbnail(path: Path) -> "PIL.Image.Image": # type: ignore # noqa: F821
89
41
  """Get thumbnail as PIL image.
90
42
 
91
43
  Args:
@@ -96,18 +48,27 @@ class Service(BaseService):
96
48
 
97
49
  Raises:
98
50
  ValueError: If the file type is not supported (.dcm, .tiff, or .tif).
51
+ RuntimeError: If there is an error generating the thumbnail.
99
52
  """
53
+ from ._openslide_handler import OpenSlideHandler # noqa: PLC0415
54
+
100
55
  if path.exists() is False:
101
56
  message = f"File does not exist: {path}"
102
57
  logger.warning(message)
103
58
  raise ValueError(message)
104
- if path.suffix.lower() in {".dcm", ".tiff", ".tif", ".svs"}:
105
- return self._get_openslide_thumbnail(path)
106
- message = f"Unsupported file type: {path.suffix}. Supported types are .dcm, .tiff, and .tif."
107
- logger.warning(message)
108
- raise ValueError(message)
59
+ if path.suffix.lower() not in FILE_TYPES_SUPPORTED:
60
+ message = f"Unsupported file type: {path.suffix}. Supported types are .dcm, .tiff, and .tif."
61
+ logger.warning(message)
62
+ raise ValueError(message)
63
+ try:
64
+ return OpenSlideHandler.from_file(path).get_thumbnail()
65
+ except Exception as e:
66
+ message = f"Error processing file {path}: {e!s}"
67
+ logger.exception(message)
68
+ raise RuntimeError(message) from e
109
69
 
110
- def get_thumbnail_bytes(self, path: Path) -> bytes:
70
+ @staticmethod
71
+ def get_thumbnail_bytes(path: Path) -> bytes:
111
72
  """Get thumbnail of a image as bytes.
112
73
 
113
74
  Args:
@@ -117,14 +78,16 @@ class Service(BaseService):
117
78
  bytes: Thumbnail of the image.
118
79
 
119
80
  Raises:
120
- ValueError: If the file type is not supported (.dcm, .tiff, or .tif).
81
+ ValueError: If the file type is not supported.
82
+ RuntimeError: If there is an error processing the file.
121
83
  """
122
- thumbnail_image = self.get_thumbnail(path)
84
+ thumbnail_image = Service.get_thumbnail(path)
123
85
  buffer = io.BytesIO()
124
86
  thumbnail_image.save(buffer, format="PNG")
125
87
  return buffer.getvalue()
126
88
 
127
- def get_metadata(self, path: Path) -> dict[str, Any]:
89
+ @staticmethod
90
+ def get_metadata(path: Path) -> dict[str, Any]:
128
91
  """Get metadata from a TIFF file.
129
92
 
130
93
  Args:
@@ -132,10 +95,30 @@ class Service(BaseService):
132
95
 
133
96
  Returns:
134
97
  dict[str, Any]: Metadata of the TIFF file.
98
+
99
+ Raises:
100
+ ValueError: If the file type is not supported.
101
+ RuntimeError: If there is an error processing the file.
135
102
  """
136
- return self._get_openslide_metadata(path)
103
+ from ._openslide_handler import OpenSlideHandler # noqa: PLC0415
104
+
105
+ if path.exists() is False:
106
+ message = f"File does not exist: {path}"
107
+ logger.warning(message)
108
+ raise ValueError(message)
109
+ if path.suffix.lower() not in FILE_TYPES_SUPPORTED:
110
+ message = f"Unsupported file type: {path.suffix}. Supported types are .dcm, .tiff, and .tif."
111
+ logger.warning(message)
112
+ raise ValueError(message)
113
+ try:
114
+ return OpenSlideHandler.from_file(path).get_metadata()
115
+ except Exception as e:
116
+ error_msg = f"Error processing file {path}: {e!s}"
117
+ logger.exception(error_msg)
118
+ raise RuntimeError(error_msg) from e
137
119
 
138
- def get_tiff_as_jpg(self, url: str) -> bytes: # noqa: PLR6301
120
+ @staticmethod
121
+ def get_tiff_as_jpg(url: str) -> bytes:
139
122
  """Get a TIFF image from a URL and convert it to JPG format.
140
123
 
141
124
  Args:
File without changes
File without changes
File without changes
File without changes