aignostics 0.2.13__tar.gz → 0.2.16__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 (130) hide show
  1. {aignostics-0.2.13 → aignostics-0.2.16}/.gitignore +1 -1
  2. {aignostics-0.2.13 → aignostics-0.2.16}/PKG-INFO +1 -1
  3. {aignostics-0.2.13 → aignostics-0.2.16}/examples/notebook.py +1 -1
  4. {aignostics-0.2.13 → aignostics-0.2.16}/pyproject.toml +3 -2
  5. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/application/_cli.py +2 -3
  6. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/application/_gui.py +78 -49
  7. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/application/_service.py +44 -31
  8. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/dataset/_gui.py +39 -14
  9. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/_frame.py +54 -16
  10. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/_theme.py +20 -12
  11. aignostics-0.2.16/src/aignostics/gui/assets/cabin-v27-latin-regular.woff2 +0 -0
  12. aignostics-0.2.16/src/aignostics/gui/assets/home-card-1.png +0 -0
  13. aignostics-0.2.16/src/aignostics/gui/assets/home-card-2.png +0 -0
  14. aignostics-0.2.16/src/aignostics/gui/assets/home-card-3.png +0 -0
  15. aignostics-0.2.16/src/aignostics/gui/assets/home-card-4.png +0 -0
  16. aignostics-0.2.16/src/aignostics/gui/assets/home-card-5.png +0 -0
  17. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/notebook/_service.py +4 -0
  18. aignostics-0.2.16/src/aignostics/qupath/__init__.py +17 -0
  19. aignostics-0.2.16/src/aignostics/qupath/_cli.py +106 -0
  20. aignostics-0.2.16/src/aignostics/qupath/_gui.py +80 -0
  21. aignostics-0.2.16/src/aignostics/qupath/_service.py +552 -0
  22. aignostics-0.2.16/src/aignostics/qupath/_settings.py +16 -0
  23. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/system/_gui.py +1 -1
  24. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_health.py +8 -0
  25. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/wsi/_service.py +1 -1
  26. aignostics-0.2.13/src/aignostics/gui/assets/home-card-1.png +0 -0
  27. aignostics-0.2.13/src/aignostics/gui/assets/home-card-2.png +0 -0
  28. aignostics-0.2.13/src/aignostics/gui/assets/home-card-3.png +0 -0
  29. aignostics-0.2.13/src/aignostics/gui/assets/home-card-4.png +0 -0
  30. {aignostics-0.2.13 → aignostics-0.2.16}/LICENSE +0 -0
  31. {aignostics-0.2.13 → aignostics-0.2.16}/README.md +0 -0
  32. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/.openapi-generator/FILES +0 -0
  33. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/.openapi-generator/VERSION +0 -0
  34. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/api/public_api.py +0 -0
  35. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/api_client.py +0 -0
  36. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/api_response.py +0 -0
  37. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/configuration.py +0 -0
  38. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/exceptions.py +0 -0
  39. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/__init__.py +0 -0
  40. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/application_read_response.py +0 -0
  41. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/application_run_status.py +0 -0
  42. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/application_version_read_response.py +0 -0
  43. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/http_validation_error.py +0 -0
  44. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/input_artifact_creation_request.py +0 -0
  45. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/input_artifact_read_response.py +0 -0
  46. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/item_creation_request.py +0 -0
  47. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/item_result_read_response.py +0 -0
  48. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/item_status.py +0 -0
  49. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/output_artifact_read_response.py +0 -0
  50. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/output_artifact_result_read_response.py +0 -0
  51. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/output_artifact_scope.py +0 -0
  52. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/payload_input_artifact.py +0 -0
  53. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/payload_item.py +0 -0
  54. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/payload_output_artifact.py +0 -0
  55. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/run_creation_request.py +0 -0
  56. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/run_creation_response.py +0 -0
  57. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/run_read_response.py +0 -0
  58. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/transfer_urls.py +0 -0
  59. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/user_payload.py +0 -0
  60. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/validation_error.py +0 -0
  61. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/models/validation_error_loc_inner.py +0 -0
  62. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/aignx/codegen/rest.py +0 -0
  63. {aignostics-0.2.13 → aignostics-0.2.16}/codegen/out/docs/PublicApi.md +0 -0
  64. {aignostics-0.2.13 → aignostics-0.2.16}/examples/__init__.py +0 -0
  65. {aignostics-0.2.13 → aignostics-0.2.16}/examples/e2e.py +0 -0
  66. {aignostics-0.2.13 → aignostics-0.2.16}/examples/notebook.ipynb +0 -0
  67. {aignostics-0.2.13 → aignostics-0.2.16}/examples/script.py +0 -0
  68. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/__init__.py +0 -0
  69. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/application/__init__.py +0 -0
  70. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/application/_settings.py +0 -0
  71. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/application/_utils.py +0 -0
  72. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/bucket/__init__.py +0 -0
  73. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/bucket/_cli.py +0 -0
  74. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/bucket/_gui.py +0 -0
  75. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/bucket/_service.py +0 -0
  76. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/bucket/_settings.py +0 -0
  77. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/cli.py +0 -0
  78. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/constants.py +0 -0
  79. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/dataset/__init__.py +0 -0
  80. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/dataset/_cli.py +0 -0
  81. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/dataset/_idc_index.py +0 -0
  82. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/dataset/_service.py +0 -0
  83. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/__init__.py +0 -0
  84. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/assets/Google-Cloud-logo.svg +0 -0
  85. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/assets/NIH-IDC-logo.svg +0 -0
  86. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/assets/image-not-found.png +0 -0
  87. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/assets/logo.png +0 -0
  88. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/assets/ruo.png +0 -0
  89. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/gui/assets/ruo.svg +0 -0
  90. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/notebook/__init__.py +0 -0
  91. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/notebook/_gui.py +0 -0
  92. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/notebook/_notebook.py +0 -0
  93. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/__init__.py +0 -0
  94. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/_authentication.py +0 -0
  95. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/_client.py +0 -0
  96. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/_constants.py +0 -0
  97. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/_messages.py +0 -0
  98. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/_service.py +0 -0
  99. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/_settings.py +0 -0
  100. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/_utils.py +0 -0
  101. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/resources/__init__.py +0 -0
  102. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/resources/applications.py +0 -0
  103. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/resources/runs.py +0 -0
  104. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/platform/resources/utils.py +0 -0
  105. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/system/__init__.py +0 -0
  106. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/system/_cli.py +0 -0
  107. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/system/_exceptions.py +0 -0
  108. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/system/_service.py +0 -0
  109. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/system/_settings.py +0 -0
  110. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/.vendored/bottle.py +0 -0
  111. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/__init__.py +0 -0
  112. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_cli.py +0 -0
  113. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_console.py +0 -0
  114. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_constants.py +0 -0
  115. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_di.py +0 -0
  116. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_gui.py +0 -0
  117. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_log.py +0 -0
  118. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_logfire.py +0 -0
  119. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_notebook.py +0 -0
  120. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_process.py +0 -0
  121. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_sentry.py +0 -0
  122. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_service.py +0 -0
  123. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/_settings.py +0 -0
  124. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/utils/boot.py +0 -0
  125. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/wsi/__init__.py +0 -0
  126. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/wsi/_cli.py +0 -0
  127. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/wsi/_gui.py +0 -0
  128. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/wsi/_openslide_handler.py +0 -0
  129. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/wsi/_pydicom_handler.py +0 -0
  130. {aignostics-0.2.13 → aignostics-0.2.16}/src/aignostics/wsi/_utils.py +0 -0
@@ -95,4 +95,4 @@ dist_vercel
95
95
  setup.sh.secret
96
96
  __marimo__
97
97
  notes.md
98
- CLAUDE.md
98
+ CLAUDE.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aignostics
3
- Version: 0.2.13
3
+ Version: 0.2.16
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.13",
5
+ # "aignostics==0.2.16",
6
6
  # ]
7
7
  # ///
8
8
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aignostics"
3
- version = "0.2.13"
3
+ version = "0.2.16"
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 = [
@@ -344,6 +344,7 @@ omit = [
344
344
  "*/_notebook.py",
345
345
  "*/_pydicom_handler.py",
346
346
  "*/_openslide_handler.py",
347
+ "*/qupath/*",
347
348
  ]
348
349
  branch = true
349
350
  parallel = true
@@ -354,7 +355,7 @@ source = ["src/"]
354
355
 
355
356
 
356
357
  [tool.bumpversion]
357
- current_version = "0.2.13"
358
+ current_version = "0.2.16"
358
359
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
359
360
  serialize = ["{major}.{minor}.{patch}"]
360
361
  search = "{current_version}"
@@ -376,7 +376,7 @@ def run_upload(
376
376
 
377
377
  total_bytes = 0
378
378
  for i, entry in enumerate(metadata_dict):
379
- source = entry["source"]
379
+ source = entry["reference"]
380
380
  source_file_path = Path(source)
381
381
  if not source_file_path.is_file():
382
382
  logger.warning("Source file '%s' (row %d) does not exist", source_file_path, i)
@@ -403,8 +403,7 @@ def run_upload(
403
403
  def update_progress(bytes_uploaded: int, source: Path, platform_bucket_url: str) -> None:
404
404
  progress.update(task, advance=bytes_uploaded, description=f"{source.name}")
405
405
  for entry in metadata_dict:
406
- if entry["source"] == str(source):
407
- entry["file_upload_progress"] = float(entry["file_upload_progress"]) + bytes_uploaded
406
+ if entry["reference"] == str(source):
408
407
  entry["platform_bucket_url"] = platform_bucket_url
409
408
  break
410
409
  write_metadata_dict_to_csv(
@@ -32,6 +32,9 @@ class PageBuilder(BasePageBuilder):
32
32
  from nicegui import app, background_tasks, binding, context, ui # noq # noqa: PLC0415
33
33
  from nicegui import run as nicegui_run # noqa: PLC0415
34
34
 
35
+ if find_spec("paquo"):
36
+ from aignostics.qupath import Service as QuPathService # noqa: PLC0415
37
+
35
38
  @binding.bindable_dataclass
36
39
  class SubmitForm:
37
40
  """Submit form."""
@@ -392,7 +395,10 @@ class PageBuilder(BasePageBuilder):
392
395
  f"Selected folder {submit_form.source} to analyze."
393
396
  ) if submit_form.wsi_step_label else None
394
397
  submit_form.wsi_next_button.enable() if submit_form.wsi_next_button else None
395
- ui.notify(f"You chose directory {submit_form.source}.", type="info")
398
+ ui.notify(
399
+ f"You chose directory {submit_form.source}. Feel free to continue to the next step.",
400
+ type="positive",
401
+ )
396
402
  else:
397
403
  submit_form.source = None
398
404
  submit_form.wsi_step_label.set_text(
@@ -413,7 +419,10 @@ class PageBuilder(BasePageBuilder):
413
419
  f"Selected folder {submit_form.source} to analyze."
414
420
  ) if submit_form.wsi_step_label else None
415
421
  submit_form.wsi_next_button.enable() if submit_form.wsi_next_button else None
416
- ui.notify(f"You chose directory {submit_form.source}.", type="info")
422
+ ui.notify(
423
+ f"You chose directory {submit_form.source}. Feel free to continue to the next step.",
424
+ type="positive",
425
+ )
417
426
 
418
427
  async def _on_wsi_next_click() -> None:
419
428
  """Handle the 'Next' button click in WSI step.
@@ -443,6 +452,7 @@ class PageBuilder(BasePageBuilder):
443
452
  Service.generate_metadata_from_source_directory,
444
453
  str(submit_form.application_version_id),
445
454
  submit_form.source,
455
+ True,
446
456
  [".*:staining_method=H&E"],
447
457
  )
448
458
  if submit_form.metadata_grid.options["rowData"] is None:
@@ -453,7 +463,8 @@ class PageBuilder(BasePageBuilder):
453
463
  submit_form.wsi_spinner.set_visibility(False)
454
464
  submit_form.metadata_grid.update()
455
465
  ui.notify(
456
- f"Found {len(submit_form.metadata_grid.options['rowData'])} slides for analysis.",
466
+ f"Found {len(submit_form.metadata_grid.options['rowData'])} slides for analysis."
467
+ "Please provide missing metadata.",
457
468
  type="positive",
458
469
  )
459
470
  stepper.next()
@@ -506,10 +517,13 @@ class PageBuilder(BasePageBuilder):
506
517
  ui.button("Close", on_click=info_dialog.close)
507
518
 
508
519
  with ui.stepper().props("vertical").classes("w-full") as stepper: # noqa: PLR1702
509
- with ui.step("Application Version"):
520
+ with ui.step("Select Application Version"):
510
521
  with ui.row().classes("w-full justify-center"):
511
522
  with ui.column():
512
- ui.label(f"Select the version of {application.name} you want to run.")
523
+ ui.label(
524
+ f"Select the version of {application.name} you want to run. Not sure? "
525
+ "Click “Next” to auto-select the latest version"
526
+ )
513
527
  ui.select(
514
528
  {version.application_version_id: version.version for version in application_versions},
515
529
  value=latest_application_version_id,
@@ -522,9 +536,9 @@ class PageBuilder(BasePageBuilder):
522
536
  "BUTTON_APPLICATION_VERSION_NEXT"
523
537
  )
524
538
 
525
- with ui.step("Whole Slide Images"):
539
+ with ui.step("Select Whole Slide Images"):
526
540
  submit_form.wsi_step_label = ui.label(
527
- "Select a folder with whole slide images you want to analyze."
541
+ "Select the folder with the whole slide images you want to analyze then click Next."
528
542
  )
529
543
  with ui.stepper_navigation():
530
544
  if "pytest" in sys.modules:
@@ -536,13 +550,21 @@ class PageBuilder(BasePageBuilder):
536
550
  submit_form.wsi_spinner.set_visibility(False)
537
551
  ui.button("Back", on_click=stepper.previous).props("flat")
538
552
 
539
- with ui.step("Metadata"):
553
+ with ui.step("Choose Images and Edit Metadata"):
540
554
  ui.markdown(
541
555
  """
542
- 1. Check extracted and provide missing metadata.
543
- Double click red cells to edit - you are done when all turned green.
544
- 2. You can exclude slides from the analysis by selecting them and clicking "Exclude slides".
545
- 3. You can revert to the original list by clicking the Back button.
556
+ The Launchpad has found all compatible slide files in your selected folder.
557
+
558
+ 1. Check the slides that have been found.
559
+ If you wish to exclude any from analysis at this point, check the boxes next to those slides
560
+ and click “Exclude” to remove them.
561
+ 2. For the slides you wish to analyze,
562
+ provide the missing metadata to finalize the upload.
563
+ Double click red cells to edit the missing data with the available options.
564
+ 3. Once all the metadata has been added and your slide selection has been finalized,
565
+ click “Next”.
566
+
567
+ You can revert to the previous step and reupload at any point by clicking “Back”.
546
568
  """
547
569
  )
548
570
 
@@ -616,7 +638,8 @@ class PageBuilder(BasePageBuilder):
616
638
  submit_form.metadata = await submit_form.metadata_grid.get_client_data()
617
639
  _upload_ui.refresh(submit_form.metadata)
618
640
  submit_form.submission_upload_button.enable()
619
- ui.notify("Prepared upload UI.", type="info")
641
+ if "pytest" in sys.modules:
642
+ ui.notify("Prepared upload UI.", type="info")
620
643
  stepper.next()
621
644
 
622
645
  async def _delete_selected() -> None:
@@ -662,7 +685,7 @@ class PageBuilder(BasePageBuilder):
662
685
  submit_form.metadata_grid = (
663
686
  ui.aggrid({
664
687
  "columnDefs": [
665
- {"headerName": "Reference", "field": "reference", "checkboxSelection": True},
688
+ {"headerName": "Reference", "field": "reference_short", "checkboxSelection": True},
666
689
  {
667
690
  "headerName": "Thumbnail",
668
691
  "field": "thumbnail",
@@ -750,7 +773,6 @@ class PageBuilder(BasePageBuilder):
750
773
  .on("selectionChanged", _handle_grid_selection_changed)
751
774
  .mark("GRID_METADATA")
752
775
  )
753
- # .style("height: auto; width: 1000px")
754
776
  # use ui timer to update the grid class depending on dark mode, with a frequency of once per second
755
777
  ui.timer(
756
778
  interval=1,
@@ -768,15 +790,33 @@ class PageBuilder(BasePageBuilder):
768
790
  with ui.stepper_navigation():
769
791
  if "pytest" in sys.modules:
770
792
  ui.button("Select", on_click=_pytest_meta, icon="folder").mark("BUTTON_PYTEST_META")
793
+ submit_form.metadata_next_button = ui.button("Next", on_click=_metadata_next)
794
+ submit_form.metadata_next_button.mark("BUTTON_METADATA_NEXT").disable()
771
795
  submit_form.metadata_exclude_button = ui.button(
772
796
  "Exclude selected", on_click=_delete_selected
773
797
  ).mark("BUTTON_DELETE_SELECTED")
774
798
  submit_form.metadata_exclude_button.set_text("Exclude")
775
799
  submit_form.metadata_exclude_button.disable()
776
- submit_form.metadata_next_button = ui.button("Next", on_click=_metadata_next)
777
- submit_form.metadata_next_button.mark("BUTTON_METADATA_NEXT").disable()
778
800
  ui.button("Back", on_click=stepper.previous).props("flat")
779
801
 
802
+ def _submit() -> None:
803
+ """Submit the application run."""
804
+ ui.notify("Submitting application run ...", type="info")
805
+ try:
806
+ run = service.application_run_submit_from_metadata(
807
+ str(submit_form.application_version_id),
808
+ submit_form.metadata or [],
809
+ )
810
+ except Exception as e: # noqa: BLE001
811
+ ui.notify(f"Failed to submit application run: {e}.", type="warning")
812
+ return
813
+ ui.notify(
814
+ f"Application run submitted with id '{run.application_run_id}'. "
815
+ "Navigating to application run ...",
816
+ type="positive",
817
+ )
818
+ ui.navigate.to(f"/application/run/{run.application_run_id}")
819
+
780
820
  async def _upload() -> None:
781
821
  """Upload prepared slides."""
782
822
  if submit_form.submission_submit_button is None or submit_form.submission_upload_button is None:
@@ -796,12 +836,13 @@ class PageBuilder(BasePageBuilder):
796
836
  ui.notify("Upload to Aignostics Platform completed.", type="positive")
797
837
  submit_form.submission_submit_button.enable()
798
838
  submit_form.submission_upload_button.disable()
839
+ _submit()
799
840
 
800
841
  @ui.refreshable
801
842
  def _upload_ui(metadata: list[dict[str, Any]]) -> None:
802
843
  """Upload UI."""
803
844
  with ui.column(align_items="start"):
804
- ui.label(f"1. Upload {len(metadata)} slides you prepared to the Aignostics Platform.")
845
+ ui.label(f"Upload and submit your {len(metadata)} slide(s) for analysis.")
805
846
  upload_complete = True
806
847
  for row in metadata or []:
807
848
  upload_complete = upload_complete and row["file_upload_progress"] == 1
@@ -809,11 +850,6 @@ class PageBuilder(BasePageBuilder):
809
850
  with ui.circular_progress(value=row["file_upload_progress"], show_value=False):
810
851
  ui.button(icon="cloud_upload").props("flat round").disable()
811
852
  ui.label(f"{row['source']} ({row['file_size_human']})").classes("w-4/5")
812
- if upload_complete:
813
- ui.label(
814
- f"2. All uploads completed successfully. Click submit to run "
815
- f"{submit_form.application_version_id} on {len(metadata)} slides."
816
- )
817
853
 
818
854
  def _update_upload_progress() -> None:
819
855
  """Update the upload progress for each file."""
@@ -832,28 +868,14 @@ class PageBuilder(BasePageBuilder):
832
868
  break
833
869
  _upload_ui.refresh(submit_form.metadata)
834
870
 
835
- def _submit() -> None:
836
- """Submit the application run."""
837
- ui.notify("Submitting application run ...", type="info")
838
- try:
839
- run = service.application_run_submit_from_metadata(
840
- str(submit_form.application_version_id),
841
- submit_form.metadata or [],
842
- )
843
- except Exception as e: # noqa: BLE001
844
- ui.notify(f"Failed to submit application run: {e}.", type="warning")
845
- return
846
- ui.notify(f"Application run submitted with id '{run.application_run_id}'.", type="positive")
847
- ui.navigate.to(f"/application/run/{run.application_run_id}")
848
-
849
- with ui.step("Submission"):
871
+ with ui.step("Slide Submission"):
850
872
  _upload_ui([])
851
873
  upload_message_queue = Manager().Queue()
852
874
  ui.timer(0.1, callback=_update_upload_progress)
853
875
 
854
876
  with ui.stepper_navigation():
855
877
  submit_form.submission_upload_button = ui.button(
856
- "Upload",
878
+ "Upload and Submit",
857
879
  on_click=_upload,
858
880
  icon="check",
859
881
  ).mark("BUTTON_SUBMISSION_UPLOAD")
@@ -862,6 +884,7 @@ class PageBuilder(BasePageBuilder):
862
884
  on_click=_submit,
863
885
  icon="check",
864
886
  )
887
+ submit_form.submission_submit_button.set_visibility(False)
865
888
  submit_form.submission_submit_button.mark("BUTTON_SUBMISSION_SUBMIT").disable()
866
889
  ui.button("Back", on_click=stepper.previous).props("flat")
867
890
 
@@ -906,8 +929,8 @@ class PageBuilder(BasePageBuilder):
906
929
  ui.notify(f"Canceling application run with id '{run_id}' ...", type="info")
907
930
  try:
908
931
  service.application_run_cancel(run_id)
909
- ui.notify("Application run cancelled!", type="positive")
910
932
  ui.navigate.reload()
933
+ ui.notify("Application run cancelled!", type="positive")
911
934
  return True
912
935
  except Exception as e: # noqa: BLE001
913
936
  ui.notify(f"Failed to cancel application run: {e}.", type="warning")
@@ -934,11 +957,14 @@ class PageBuilder(BasePageBuilder):
934
957
  selected_folder.value = str(Path.home())
935
958
 
936
959
  ui.label("Select a folder to download all results for this application run.")
937
- ui.button("Use Home", on_click=_select_home, icon="home").mark("BUTTON_DOWNLOAD_DESTINATION_HOME")
938
- ui.space()
939
- ui.button("Select Download Folder", on_click=_select_download_destination, icon="folder_open").mark(
940
- "BUTTON_DOWNLOAD_DESTINATION_SELECT"
941
- )
960
+ with ui.row().classes("w-full"):
961
+ ui.button("Use Home", on_click=_select_home, icon="home").mark(
962
+ "BUTTON_DOWNLOAD_DESTINATION_HOME"
963
+ )
964
+ ui.space()
965
+ ui.button(
966
+ "Select Download Folder", on_click=_select_download_destination, icon="folder_open"
967
+ ).mark("BUTTON_DOWNLOAD_DESTINATION_SELECT")
942
968
 
943
969
  download_item_status = ui.label("")
944
970
  download_item_status.set_visibility(False)
@@ -1004,7 +1030,7 @@ class PageBuilder(BasePageBuilder):
1004
1030
  ui.notify("Download completed.", type="positive")
1005
1031
 
1006
1032
  ui.separator()
1007
- with ui.row(align_items="end").classes("w-full"):
1033
+ with ui.row(align_items="end").classes("w-full justify-end"):
1008
1034
  ui.button("Close", on_click=download_run_dialog.close, color="secondary").props("outline").mark(
1009
1035
  "DIALOG_BUTTON_DOWNLOAD_CLOSE"
1010
1036
  )
@@ -1013,7 +1039,7 @@ class PageBuilder(BasePageBuilder):
1013
1039
  "DIALOG_BUTTON_DOWNLOAD_RUN"
1014
1040
  )
1015
1041
 
1016
- with ui.dialog() as qupath_project_create_dialog, ui.card().style(WIDTH_1200px):
1042
+ with ui.dialog() as qupath_project_create_dialog, ui.card().style(WIDTH_1200px): # noqa: F841
1017
1043
  ui.button("Select QuPath folder")
1018
1044
  with ui.row(align_items="end").classes("w-full"), ui.column(align_items="end").classes("w-full"):
1019
1045
  ui.button("Close", on_click=download_run_dialog.close)
@@ -1078,11 +1104,14 @@ class PageBuilder(BasePageBuilder):
1078
1104
 
1079
1105
  if run_data and run_data.status.value == ApplicationRunStatus.COMPLETED:
1080
1106
  with ui.row().classes("w-full justify-end"):
1081
- if find_spec("paquo"):
1107
+ if find_spec("paquo") and QuPathService.find_qupath():
1082
1108
  ui.button(
1083
1109
  "Open in QuPath Microscopy Viewer",
1084
1110
  icon="zoom_in",
1085
- on_click=qupath_project_create_dialog.open,
1111
+ on_click=lambda: (
1112
+ ui.notify("Opening QuPath ...", type="info"), # type: ignore[func-returns-value]
1113
+ QuPathService.launch_qupath(),
1114
+ ),
1086
1115
  )
1087
1116
  if find_spec("marimo"):
1088
1117
  ui.button(
@@ -311,12 +311,13 @@ class Service(BaseService):
311
311
  def generate_metadata_from_source_directory(
312
312
  application_version_id: str,
313
313
  source_directory: Path,
314
+ with_gui_metadata: bool = False,
314
315
  mappings: list[str] | None = None,
315
316
  ) -> list[dict[str, Any]]:
316
317
  """Generate metadata from the source directory.
317
318
 
318
319
  Steps:
319
- 1. Recursively files ending with .tiff, .tif and .dcm in the source directory
320
+ 1. Recursively files ending with .tiff, .tif, .svs and .dcm in the source directory
320
321
  2. Creates a dict with the following columns
321
322
  - reference (str): The reference of the file, by default equivalent to the absolute file name
322
323
  - source (str): The absolute filename
@@ -331,6 +332,7 @@ class Service(BaseService):
331
332
  application_version_id (str): The ID of the application version.
332
333
  If application id is given, the latest version of that application is used.
333
334
  source_directory (Path): The source directory to generate metadata from.
335
+ with_gui_metadata (bool): If True, include additional metadata for GUI.
334
336
  mappings (list[str]): Mappings of the form '<regexp>:<key>:<value>,<key>:<value>,...'.
335
337
  The regular expression is matched against the reference attribute of the entry.
336
338
  The key/value pairs are applied to the entry if the pattern matches.
@@ -350,7 +352,7 @@ class Service(BaseService):
350
352
  application_version = Service().application_version(application_version_id, use_latest_if_no_version_given=True) # noqa: F841
351
353
 
352
354
  metadata = []
353
- file_extensions = [".tiff", ".tif", ".dcm"]
355
+ file_extensions = [".tiff", ".tif", ".svs", ".dcm"]
354
356
 
355
357
  try:
356
358
  for extension in file_extensions:
@@ -361,31 +363,42 @@ class Service(BaseService):
361
363
  while chunk := f.read(1024):
362
364
  hash_sum.update(chunk) # type: ignore[no-untyped-call]
363
365
  checksum = str(base64.b64encode(hash_sum.digest()), "UTF-8") # type: ignore[no-untyped-call]
364
- image_metadata = WSIService().get_metadata(file_path)
365
- width = image_metadata["dimensions"]["width"]
366
- height = image_metadata["dimensions"]["height"]
367
- mpp = image_metadata["resolution"]["mpp_x"]
368
- file_size_human = image_metadata["file"]["size_human"]
369
- reference = file_path.absolute()
370
- entry = {
371
- "reference": str(reference),
372
- "source": str(file_path),
373
- "checksum_base64_crc32c": checksum,
374
- "resolution_mpp": mpp,
375
- "width_px": width,
376
- "height_px": height,
377
- "staining_method": None,
378
- "tissue": None,
379
- "disease": None,
380
- "file_size_human": file_size_human,
381
- "file_upload_progress": 0.0,
382
- "platform_bucket_url": None,
383
- }
384
-
385
- if mappings:
386
- Service._apply_mappings_to_entry(entry, mappings)
387
-
388
- metadata.append(entry)
366
+ try:
367
+ image_metadata = WSIService().get_metadata(file_path)
368
+ width = image_metadata["dimensions"]["width"]
369
+ height = image_metadata["dimensions"]["height"]
370
+ mpp = image_metadata["resolution"]["mpp_x"]
371
+ file_size_human = image_metadata["file"]["size_human"]
372
+ reference = file_path.absolute()
373
+ entry = {
374
+ "reference": str(reference),
375
+ "reference_short": str(reference.name),
376
+ "source": str(file_path),
377
+ "checksum_base64_crc32c": checksum,
378
+ "resolution_mpp": mpp,
379
+ "width_px": width,
380
+ "height_px": height,
381
+ "staining_method": None,
382
+ "tissue": None,
383
+ "disease": None,
384
+ "file_size_human": file_size_human,
385
+ "file_upload_progress": 0.0,
386
+ "platform_bucket_url": None,
387
+ }
388
+ if not with_gui_metadata:
389
+ entry.pop("reference_short", None)
390
+ entry.pop("source", None)
391
+ entry.pop("file_size_human", None)
392
+ entry.pop("file_upload_progress", None)
393
+
394
+ if mappings:
395
+ Service._apply_mappings_to_entry(entry, mappings)
396
+
397
+ metadata.append(entry)
398
+ except Exception:
399
+ message = f"Failed to process file '{file_path}'"
400
+ logger.exception(message)
401
+ continue
389
402
 
390
403
  logger.debug("Generated metadata for %d files", len(metadata))
391
404
  return metadata
@@ -421,9 +434,9 @@ class Service(BaseService):
421
434
  application_version = Service().application_version(application_version_id, use_latest_if_no_version_given=True)
422
435
  for row in metadata:
423
436
  reference = row["reference"]
424
- source_file_path = Path(row["source"])
437
+ source_file_path = Path(row["reference"])
425
438
  if not source_file_path.is_file():
426
- logger.warning("Source file '%s' does not exist.", row["source"])
439
+ logger.warning("Source file '%s' does not exist.", row["referebce"])
427
440
  return False
428
441
  object_key = (
429
442
  f"{psutil.Process().username()}/{upload_prefix}/"
@@ -583,9 +596,9 @@ class Service(BaseService):
583
596
  "width_px": int(row["width_px"]),
584
597
  "media_type": (
585
598
  "image/tiff"
586
- if row["source"].lower().endswith((".tif", ".tiff"))
599
+ if row["reference"].lower().endswith((".tif", ".tiff"))
587
600
  else "application/dicom"
588
- if row["source"].lower().endswith(".dcm")
601
+ if row["reference"].lower().endswith(".dcm")
589
602
  else "application/octet-stream"
590
603
  ),
591
604
  "resolution_mpp": float(row["resolution_mpp"]),
@@ -42,21 +42,46 @@ class PageBuilder(BasePageBuilder):
42
42
  # No need to do anything here
43
43
  pass
44
44
  with ui.row(align_items="start").classes("full-width"):
45
- ui.markdown("""
46
- ##### Download DICOM datasets from IDC Portal of NCI
47
- 1. Click 🔍 Explore Portal to find DICOM datasets of interest.
48
- 2. Find a collection, patient case, study, series, or instance of interest,
49
- and copy one or multiple IDs into the field below.
50
- 3. If you don't want to explore, simply click on "Example Dataset".
51
- 4. Use ⌂ Home or 📁 select a download folder and hit ↓ download.
52
- 5. Run Applications via the ☰ menu, select an application, and the folder
53
- """).classes("w-3/5")
45
+ ui.markdown(
46
+ """
47
+ ### Download public DICOM datasets
48
+ Explore the IDC Portal from NCI to find datasets of interest and upload them to the Launchpad
49
+ to test Aignostics applications.
50
+ """
51
+ ).classes("w-3/5")
54
52
  ui.space()
55
53
  with ui.column().classes("w-1/5"):
56
54
  ui.image("/assets/NIH-IDC-logo.svg").classes("w-25").style("margin-top:1.25rem")
57
55
  with ui.link(target=PORTAL_URL, new_tab=True):
58
56
  ui.button("Explore Portal", icon="search")
59
57
 
58
+ with ui.row(align_items="start").classes("w-full"):
59
+ ui.markdown("""
60
+ ##### Pick a Dataset
61
+ 1. Click “Explore Portal” on the right and find a DICOM dataset of interest
62
+ 2. Copy the UID into the field below
63
+ """).classes("w-2/5")
64
+ ui.space()
65
+ ui.label("OR").style("margin-top: 2rem; font-size: 2rem; color: grey")
66
+ ui.space()
67
+ ui.markdown("""
68
+ ##### Quick Start
69
+ 1. Click “Use Example Dataset” below to automatically populate a pre-selected DICOM dataset
70
+ """).classes("w-2/5")
71
+
72
+ with ui.row(align_items="start").classes("w-full"):
73
+ ui.markdown(
74
+ """
75
+ Once your dataset UID is added, click "Use Home" or “Select Download Folder” and select a
76
+ folder on your computer for the dataset to be stored.
77
+ Once you've chosen, the “Download” button should light up and you can download your
78
+ selected dataset.
79
+
80
+ When you're ready to begin image analysis, open ☰ menu and click “Run Applications” to return to
81
+ the main menu.
82
+ """
83
+ )
84
+
60
85
  def _on_source_input_change(e: ValueChangeEventArguments) -> None:
61
86
  """On change event."""
62
87
  if download_form.download_button is None:
@@ -192,6 +217,11 @@ class PageBuilder(BasePageBuilder):
192
217
  color="secondary",
193
218
  ).mark("BUTTON_EXAMPLE_DATASET")
194
219
  ui.space()
220
+ ui.button("Use Home", on_click=_select_home, icon="home").mark("BUTTON_DOWNLOAD_DESTINATION_HOME")
221
+ ui.button("Select Download Folder", on_click=_select_destination, icon="folder").mark(
222
+ "BUTTON_DOWNLOAD_DESTINATION"
223
+ )
224
+ ui.space()
195
225
  with ui.row(align_items="center"):
196
226
  download_form.download_button = ui.button("Download", icon="cloud_download").mark(
197
227
  "BUTTON_DOWNLOAD"
@@ -204,11 +234,6 @@ class PageBuilder(BasePageBuilder):
204
234
  with download_form.download_progress:
205
235
  ui.button(icon="cloud_download").props("flat round").disable()
206
236
  download_form.download_progress.visible = False
207
- ui.space()
208
- ui.button("Use Home", on_click=_select_home, icon="home").mark("BUTTON_DOWNLOAD_DESTINATION_HOME")
209
- ui.button("Select Download Folder", on_click=_select_destination, icon="folder").mark(
210
- "BUTTON_DOWNLOAD_DESTINATION"
211
- )
212
237
 
213
238
  download_message_queue = Manager().Queue()
214
239