aignostics 0.2.27__tar.gz → 0.2.30__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 (146) hide show
  1. {aignostics-0.2.27 → aignostics-0.2.30}/PKG-INFO +1 -1
  2. {aignostics-0.2.27 → aignostics-0.2.30}/examples/notebook.py +1 -1
  3. {aignostics-0.2.27 → aignostics-0.2.30}/pyproject.toml +2 -2
  4. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_service.py +19 -7
  5. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/gui/_frame.py +3 -3
  6. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/system/_cli.py +33 -44
  7. aignostics-0.2.30/src/aignostics/system/_gui.py +93 -0
  8. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/system/_service.py +85 -0
  9. aignostics-0.2.30/src/aignostics/system/assets/system.lottie +0 -0
  10. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/__init__.py +3 -0
  11. aignostics-0.2.30/src/aignostics/utils/_fs.py +74 -0
  12. aignostics-0.2.27/src/aignostics/system/_gui.py +0 -61
  13. {aignostics-0.2.27 → aignostics-0.2.30}/.gitignore +0 -0
  14. {aignostics-0.2.27 → aignostics-0.2.30}/LICENSE +0 -0
  15. {aignostics-0.2.27 → aignostics-0.2.30}/README.md +0 -0
  16. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/.openapi-generator/FILES +0 -0
  17. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/.openapi-generator/VERSION +0 -0
  18. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/api/public_api.py +0 -0
  19. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/api_client.py +0 -0
  20. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/api_response.py +0 -0
  21. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/configuration.py +0 -0
  22. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/exceptions.py +0 -0
  23. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/__init__.py +0 -0
  24. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/application_read_response.py +0 -0
  25. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/application_run_status.py +0 -0
  26. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/application_version_read_response.py +0 -0
  27. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/http_validation_error.py +0 -0
  28. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/input_artifact_creation_request.py +0 -0
  29. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/input_artifact_read_response.py +0 -0
  30. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/item_creation_request.py +0 -0
  31. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/item_result_read_response.py +0 -0
  32. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/item_status.py +0 -0
  33. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/output_artifact_read_response.py +0 -0
  34. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/output_artifact_result_read_response.py +0 -0
  35. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/output_artifact_scope.py +0 -0
  36. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/payload_input_artifact.py +0 -0
  37. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/payload_item.py +0 -0
  38. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/payload_output_artifact.py +0 -0
  39. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/run_creation_request.py +0 -0
  40. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/run_creation_response.py +0 -0
  41. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/run_read_response.py +0 -0
  42. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/transfer_urls.py +0 -0
  43. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/user_payload.py +0 -0
  44. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/validation_error.py +0 -0
  45. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/models/validation_error_loc_inner.py +0 -0
  46. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/aignx/codegen/rest.py +0 -0
  47. {aignostics-0.2.27 → aignostics-0.2.30}/codegen/out/docs/PublicApi.md +0 -0
  48. {aignostics-0.2.27 → aignostics-0.2.30}/examples/__init__.py +0 -0
  49. {aignostics-0.2.27 → aignostics-0.2.30}/examples/e2e.py +0 -0
  50. {aignostics-0.2.27 → aignostics-0.2.30}/examples/notebook.ipynb +0 -0
  51. {aignostics-0.2.27 → aignostics-0.2.30}/examples/script.py +0 -0
  52. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/__init__.py +0 -0
  53. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/__init__.py +0 -0
  54. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_cli.py +0 -0
  55. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/_frame.py +0 -0
  56. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/_page_application_describe.py +0 -0
  57. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/_page_application_run_describe.py +0 -0
  58. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/_page_builder.py +0 -0
  59. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/_page_index.py +0 -0
  60. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/_utils.py +0 -0
  61. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/canceled.lottie +0 -0
  62. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/done.lottie +0 -0
  63. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/empty.lottie +0 -0
  64. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/error.lottie +0 -0
  65. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/error_alt.lottie +0 -0
  66. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/home-card-1.png +0 -0
  67. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/home-card-2.png +0 -0
  68. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/home-card-3.png +0 -0
  69. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/home-card-4.png +0 -0
  70. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/home-card-5.png +0 -0
  71. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/image-not-found.png +0 -0
  72. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/pending.lottie +0 -0
  73. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/pending_alt.lottie +0 -0
  74. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/ruo.png +0 -0
  75. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_gui/assets/ruo.svg +0 -0
  76. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_settings.py +0 -0
  77. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/application/_utils.py +0 -0
  78. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/bucket/__init__.py +0 -0
  79. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/bucket/_cli.py +0 -0
  80. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/bucket/_gui.py +0 -0
  81. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/bucket/_service.py +0 -0
  82. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/bucket/_settings.py +0 -0
  83. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/bucket/assets/Google-Cloud-logo.png +0 -0
  84. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/bucket/assets/Google-Cloud-logo.svg +0 -0
  85. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/cli.py +0 -0
  86. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/constants.py +0 -0
  87. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/dataset/__init__.py +0 -0
  88. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/dataset/_cli.py +0 -0
  89. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/dataset/_gui.py +0 -0
  90. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/dataset/_idc_index.py +0 -0
  91. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/dataset/_service.py +0 -0
  92. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/dataset/assets/NIH-IDC-logo.svg +0 -0
  93. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/gui/__init__.py +0 -0
  94. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/gui/_theme.py +0 -0
  95. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/gui/assets/cabin-v27-latin-regular.woff2 +0 -0
  96. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/gui/assets/logo.png +0 -0
  97. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/notebook/__init__.py +0 -0
  98. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/notebook/_gui.py +0 -0
  99. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/notebook/_notebook.py +0 -0
  100. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/notebook/_service.py +0 -0
  101. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/notebook/assets/python.lottie +0 -0
  102. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/__init__.py +0 -0
  103. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/_authentication.py +0 -0
  104. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/_client.py +0 -0
  105. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/_constants.py +0 -0
  106. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/_messages.py +0 -0
  107. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/_service.py +0 -0
  108. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/_settings.py +0 -0
  109. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/_utils.py +0 -0
  110. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/resources/__init__.py +0 -0
  111. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/resources/applications.py +0 -0
  112. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/resources/runs.py +0 -0
  113. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/platform/resources/utils.py +0 -0
  114. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/qupath/__init__.py +0 -0
  115. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/qupath/_cli.py +0 -0
  116. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/qupath/_gui.py +0 -0
  117. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/qupath/_service.py +0 -0
  118. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/qupath/_settings.py +0 -0
  119. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/qupath/assets/download.lottie +0 -0
  120. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/qupath/assets/microscope.lottie +0 -0
  121. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/system/__init__.py +0 -0
  122. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/system/_exceptions.py +0 -0
  123. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/system/_settings.py +0 -0
  124. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/.vendored/bottle.py +0 -0
  125. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_cli.py +0 -0
  126. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_console.py +0 -0
  127. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_constants.py +0 -0
  128. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_di.py +0 -0
  129. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_gui.py +0 -0
  130. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_health.py +0 -0
  131. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_log.py +0 -0
  132. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_logfire.py +0 -0
  133. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_notebook.py +0 -0
  134. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_process.py +0 -0
  135. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_sentry.py +0 -0
  136. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_service.py +0 -0
  137. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/_settings.py +0 -0
  138. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/utils/boot.py +0 -0
  139. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/__init__.py +0 -0
  140. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/_cli.py +0 -0
  141. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/_gui.py +0 -0
  142. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/_openslide_handler.py +0 -0
  143. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/_pydicom_handler.py +0 -0
  144. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/_service.py +0 -0
  145. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/_utils.py +0 -0
  146. {aignostics-0.2.27 → aignostics-0.2.30}/src/aignostics/wsi/assets/fallback.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aignostics
3
- Version: 0.2.27
3
+ Version: 0.2.30
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.27",
5
+ # "aignostics==0.2.30",
6
6
  # ]
7
7
  # ///
8
8
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aignostics"
3
- version = "0.2.27"
3
+ version = "0.2.30"
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.27"
358
+ current_version = "0.2.30"
359
359
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
360
360
  serialize = ["{major}.{minor}.{patch}"]
361
361
  search = "{current_version}"
@@ -29,9 +29,9 @@ from aignostics.platform import (
29
29
  ItemResult,
30
30
  ItemStatus,
31
31
  NotFoundException,
32
- OutputArtifactData,
32
+ OutputArtifactElement,
33
33
  )
34
- from aignostics.utils import BaseService, Health, get_logger
34
+ from aignostics.utils import BaseService, Health, get_logger, sanitize_path_component
35
35
  from aignostics.wsi import Service as WSIService
36
36
 
37
37
  from ._settings import Settings
@@ -73,7 +73,7 @@ class DownloadProgress(BaseModel):
73
73
  item_count: int | None = None
74
74
  item_index: int | None = None
75
75
  item_reference: str | None = None
76
- artifact: OutputArtifactData | None = None
76
+ artifact: OutputArtifactElement | None = None
77
77
  artifact_count: int | None = None
78
78
  artifact_index: int | None = None
79
79
  artifact_path: Path | None = None
@@ -705,7 +705,7 @@ class Service(BaseService):
705
705
  download_progress_queue,
706
706
  )
707
707
 
708
- def application_run_download( # noqa: C901, PLR0912, PLR0913, PLR0915, PLR0917
708
+ def application_run_download( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
709
709
  self,
710
710
  run_id: str,
711
711
  destination_directory: Path,
@@ -876,12 +876,19 @@ class Service(BaseService):
876
876
  get_mime_type_for_artifact(artifact) == "application/geo+json"
877
877
  and artifact.name == "cell_classification:geojson_polygons"
878
878
  ):
879
+ artifact_name = artifact.name
879
880
  if create_subdirectory_per_item:
880
881
  reference_path = Path(item.reference)
881
882
  stem_name = reference_path.stem
882
- artifact_path = final_destination_directory / stem_name / f"{artifact.name}.json"
883
+ artifact_path = (
884
+ final_destination_directory
885
+ / stem_name
886
+ / f"{sanitize_path_component(artifact_name)}.json"
887
+ )
883
888
  else:
884
- artifact_path = final_destination_directory / f"{artifact.name}.json"
889
+ artifact_path = (
890
+ final_destination_directory / f"{sanitize_path_component(artifact_name)}.json"
891
+ )
885
892
  message = f"Annotating input slide '{image_path}' with artifact '{artifact_path}' ..."
886
893
  logger.debug(message)
887
894
  added = QuPathService.annotate(
@@ -943,6 +950,8 @@ class Service(BaseService):
943
950
  progress.status = DownloadProgressState.DOWNLOADING
944
951
  progress.item_index = item_index
945
952
  progress.item = item
953
+ progress.item_reference = item.reference
954
+
946
955
  progress.artifact_count = len(item.output_artifacts)
947
956
  Service._update_progress(progress, download_progress_callable, download_progress_queue)
948
957
 
@@ -1007,7 +1016,10 @@ class Service(BaseService):
1007
1016
  logger.error(message)
1008
1017
  raise ValueError(message)
1009
1018
 
1010
- artifact_path = destination_directory / f"{prefix}{artifact.name}{get_file_extension_for_artifact(artifact)}"
1019
+ artifact_path = (
1020
+ destination_directory
1021
+ / f"{prefix}{sanitize_path_component(artifact.name)}{get_file_extension_for_artifact(artifact)}"
1022
+ )
1011
1023
 
1012
1024
  if artifact_path.exists():
1013
1025
  checksum = google_crc32c.Checksum() # type: ignore[no-untyped-call]
@@ -44,9 +44,9 @@ def frame( # noqa: C901, PLR0915
44
44
  @ui.refreshable
45
45
  def health_icon() -> None:
46
46
  if launchpad_healthy:
47
- ui.icon("check_circle", color="positive")
47
+ ui.icon("settings", color="positive")
48
48
  elif launchpad_healthy is not None:
49
- ui.icon("error", color="negative")
49
+ ui.icon("settings", color="negative")
50
50
 
51
51
  @ui.refreshable
52
52
  def health_link() -> None:
@@ -215,7 +215,7 @@ def frame( # noqa: C901, PLR0915
215
215
  with ui.item_section().props("avatar"):
216
216
  health_icon()
217
217
  with ui.item_section():
218
- ui.label("Check Launchpad Status").tailwind.font_weight(
218
+ ui.label("Info and Settings").tailwind.font_weight(
219
219
  "bold" if context.client.page.path == "/system" else "normal"
220
220
  )
221
221
 
@@ -4,14 +4,13 @@ import json
4
4
  import sys
5
5
  from enum import StrEnum
6
6
  from importlib.util import find_spec
7
- from pathlib import Path
8
7
  from typing import Annotated
9
8
 
10
9
  import typer
11
10
  import yaml
12
11
 
13
12
  from ..constants import API_VERSIONS # noqa: TID252
14
- from ..utils import __project_name__, console, get_logger # noqa: TID252
13
+ from ..utils import console, get_logger # noqa: TID252
15
14
  from ._service import Service
16
15
 
17
16
  logger = get_logger(__name__)
@@ -160,7 +159,7 @@ cli.add_typer(config_app, name="config", help="Configure application settings.")
160
159
  @config_app.command()
161
160
  def get(key: Annotated[str, typer.Argument(help="Configuration key to get value for")]) -> None:
162
161
  """Set a configuration key to a value."""
163
- console.print(Service().dotenv_get(key.upper()))
162
+ console.print(Service.dotenv_get(key.upper()))
164
163
 
165
164
 
166
165
  @config_app.command()
@@ -170,7 +169,11 @@ def set( # noqa: A001
170
169
  ) -> None:
171
170
  """Set a configuration key to a value."""
172
171
  key = key.upper()
173
- Service().dotenv_set(key, value)
172
+ try:
173
+ Service.dotenv_set(key, value)
174
+ except ValueError as e:
175
+ console.print(f"Invalid configuration: {e!s}", style="error")
176
+ sys.exit(2)
174
177
  console.print(f"Configuration '{key}' set to '{value}'.", style="success")
175
178
 
176
179
 
@@ -180,24 +183,30 @@ def unset(
180
183
  ) -> None:
181
184
  """Set a configuration key to a value."""
182
185
  key = key.upper()
183
- Service().dotenv_unset(key)
186
+ Service.dotenv_unset(key)
184
187
  console.print(f"Configuration '{key}' unset.", style="success")
185
188
 
186
189
 
187
190
  @config_app.command()
188
191
  def remote_diagnostics_enable() -> None:
189
192
  """Enable remote diagnostics via Sentry and Logfire. Data stored in EU data centers."""
190
- Service().dotenv_set(f"{__project_name__.upper()}_SENTRY_ENABLED", "1")
191
- Service().dotenv_set(f"{__project_name__.upper()}_LOGFIRE_ENABLED", "1")
192
- console.print("Remote diagnostics enabled.", style="success")
193
+ try:
194
+ Service.remote_diagnostics_enable()
195
+ console.print("Remote diagnostics enabled.", style="success")
196
+ except ValueError as e:
197
+ console.print(f"Invalid diagnostics configuration: {e!s}", style="error")
198
+ sys.exit(2)
193
199
 
194
200
 
195
201
  @config_app.command()
196
202
  def remote_diagnostics_disable() -> None:
197
203
  """Disable remote diagnostics."""
198
- Service().dotenv_unset(f"{__project_name__.upper()}_SENTRY_ENABLED")
199
- Service().dotenv_unset(f"{__project_name__.upper()}_LOGFIRE_ENABLED")
200
- console.print("Remote diagnostics disabled.", style="success")
204
+ try:
205
+ Service.remote_diagnostics_disable()
206
+ console.print("Remote diagnostics disabled.", style="success")
207
+ except ValueError as e:
208
+ console.print(f"Invalid diagnostics configuration: {e!s}", style="error")
209
+ sys.exit(2)
201
210
 
202
211
 
203
212
  @config_app.command()
@@ -209,42 +218,22 @@ def http_proxy_enable(
209
218
  no_ssl_verify: Annotated[bool, typer.Option(help="Disable SSL verification")] = False,
210
219
  ) -> None:
211
220
  """Enable HTTP proxy."""
212
- url = f"{scheme}://{host}:{port}"
213
- Service().dotenv_set("HTTP_PROXY", url)
214
- Service().dotenv_set("HTTPS_PROXY", url)
215
- if ssl_cert_file is not None and no_ssl_verify:
216
- message = "Cannot set both 'ssl_cert_file' and 'ssl_disable_verify'. Please choose one."
217
- console.print(message, style="warning")
221
+ try:
222
+ Service().http_proxy_enable(
223
+ host=host, port=port, scheme=scheme, ssl_cert_file=ssl_cert_file, no_ssl_verify=no_ssl_verify
224
+ )
225
+ console.print("HTTP proxy enabled.", style="success")
226
+ except ValueError as e:
227
+ console.print(f"Invalid HTTP proxy configuration: {e!s}", style="error")
218
228
  sys.exit(2)
219
- if no_ssl_verify:
220
- Service().dotenv_set("SSL_NO_VERIFY", "1")
221
- Service().dotenv_set("SSL_CERT_FILE", "")
222
- Service().dotenv_set("REQUESTS_CA_BUNDLE", "")
223
- Service().dotenv_set("CURL_CA_BUNDLE", "")
224
- else:
225
- Service().dotenv_unset("SSL_NO_VERIFY")
226
- Service().dotenv_unset("SSL_CERT_FILE")
227
- Service().dotenv_unset("REQUESTS_CA_BUNDLE")
228
- Service().dotenv_unset("CURL_CA_BUNDLE")
229
- if ssl_cert_file:
230
- file = Path(ssl_cert_file).resolve()
231
- if not file.is_file():
232
- message = f"SSL certificate file '{ssl_cert_file}' does not exist."
233
- console.print(message, style="error")
234
- sys.exit(2)
235
- Service().dotenv_set("SSL_CERT_FILE", str(ssl_cert_file))
236
- Service().dotenv_set("REQUESTS_CA_BUNDLE", str(ssl_cert_file))
237
- Service().dotenv_set("CURL_CA_BUNDLE", str(ssl_cert_file))
238
- console.print("HTTP proxy enabled.", style="success")
239
229
 
240
230
 
241
231
  @config_app.command()
242
232
  def http_proxy_disable() -> None:
243
233
  """Disable HTTP proxy."""
244
- Service().dotenv_unset("HTTP_PROXY")
245
- Service().dotenv_unset("HTTPS_PROXY")
246
- Service().dotenv_unset("SSL_CERT_FILE")
247
- Service().dotenv_unset("SSL_NO_VERIFY")
248
- Service().dotenv_unset("REQUESTS_CA_BUNDLE")
249
- Service().dotenv_unset("CURL_CA_BUNDLE")
250
- console.print("HTTP proxy disabled.", style="success")
234
+ try:
235
+ Service.http_proxy_disable()
236
+ console.print("HTTP proxy disabled.", style="success")
237
+ except ValueError as e:
238
+ console.print(f"Invalid HTTP proxy configuration: {e!s}", style="error")
239
+ sys.exit(2)
@@ -0,0 +1,93 @@
1
+ """Homepage (index) of GUI."""
2
+
3
+ from pathlib import Path
4
+
5
+ from aignostics.gui import frame
6
+ from aignostics.utils import BaseService, locate_subclasses
7
+
8
+ from ..utils import BasePageBuilder # noqa: TID252
9
+ from ._service import Service
10
+
11
+
12
+ class PageBuilder(BasePageBuilder):
13
+ @staticmethod
14
+ def register_pages() -> None:
15
+ from nicegui import app, run, ui # noqa: PLC0415
16
+
17
+ locate_subclasses(BaseService) # Ensure settings are loaded
18
+ app.add_static_files("/system_assets", Path(__file__).parent / "assets")
19
+
20
+ ui.add_head_html("""
21
+ <style>
22
+ :global(.jse-modal-window.jse-modal-window-jsoneditor)
23
+ {
24
+ width: 100%;
25
+ height: 100%;
26
+ min-height: 900px;
27
+ }
28
+ </style>
29
+ """)
30
+
31
+ @ui.page("/system")
32
+ async def page_system() -> None:
33
+ """System info and settings page."""
34
+ with frame("Info and Settings", left_sidebar=False):
35
+ pass
36
+
37
+ with ui.row().classes("w-full gap-4 flex-nowrap"):
38
+ with ui.column().classes("w-3/5 flex-shrink-0"):
39
+ with ui.tabs().classes("w-full") as tabs:
40
+ tab_health = ui.tab("Health")
41
+ tab_info = ui.tab("Info")
42
+ tab_settings = ui.tab("Settings")
43
+ with ui.tab_panels(tabs, value=tab_health).classes("w-full"):
44
+ with ui.tab_panel(tab_health):
45
+ properties = {
46
+ "content": {"json": Service().health().model_dump()},
47
+ "mode": "tree",
48
+ "readOnly": True,
49
+ "mainMenuBar": False,
50
+ "navigationBar": False,
51
+ "statusBar": False,
52
+ }
53
+ ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
54
+ with ui.tab_panel(tab_info):
55
+ spinner = ui.spinner("dots", size="lg", color="red")
56
+ properties = {
57
+ "content": {"json": "Loading ..."},
58
+ "mode": "tree",
59
+ "readOnly": True,
60
+ "mainMenuBar": False,
61
+ "navigationBar": False,
62
+ "statusBar": False,
63
+ }
64
+ editor = ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
65
+ editor.set_visibility(False)
66
+ info = await run.cpu_bound(Service().info, True, True)
67
+ properties["content"] = {"json": info}
68
+ editor.update()
69
+ editor.run_editor_method(":expand", "path => true")
70
+ spinner.delete()
71
+ editor.set_visibility(True)
72
+ with (
73
+ ui.tab_panel(tab_settings),
74
+ ui.card().classes("w-full"),
75
+ ui.row().classes("items-center justify-between"),
76
+ ):
77
+ ui.switch(
78
+ value=Service.remote_diagnostics_enabled(),
79
+ on_change=lambda e: (
80
+ Service.remote_diagnostics_enable()
81
+ if e.value
82
+ else Service.remote_diagnostics_disable(),
83
+ ui.notify("Restart the app to apply changes.", color="warning"), # type: ignore[func-returns-value]
84
+ None,
85
+ )[0],
86
+ )
87
+ ui.label("Remote Diagnostics")
88
+ with ui.column().classes("w-2/5 flex-shrink-0 flex items-center justify-start mt-[200px]"):
89
+ ui.html(
90
+ '<dotlottie-player src="/system_assets/system.lottie" '
91
+ 'background="transparent" speed="1" style="width: 300px; height: 300px" '
92
+ 'direction="1" playMode="normal" loop autoplay></dotlottie-player>'
93
+ )
@@ -446,3 +446,88 @@ class Service(BaseService):
446
446
  dotenv_unset_key(dotenv_path=str(dotenv_path.resolve()), key_to_unset=key, quote_mode="never")
447
447
  os.environ.pop(key, None)
448
448
  return removed_count
449
+
450
+ @staticmethod
451
+ def remote_diagnostics_enabled() -> bool:
452
+ """Check if remote diagnostics are enabled.
453
+
454
+ Returns:
455
+ bool: True if remote diagnostics are enabled, False otherwise.
456
+ """
457
+ return (
458
+ Service.dotenv_get(f"{__project_name__.upper()}_SENTRY_ENABLED") == "1"
459
+ and Service.dotenv_get(f"{__project_name__.upper()}_LOGFIRE_ENABLED") == "1"
460
+ )
461
+
462
+ @staticmethod
463
+ def remote_diagnostics_enable() -> None:
464
+ """Enable remote diagnostics via Sentry and Logfire. Data stored in EU data centers.
465
+
466
+ Raises:
467
+ ValueError: If the environment variable cannot be set.
468
+ """
469
+ Service.dotenv_set(f"{__project_name__.upper()}_SENTRY_ENABLED", "1")
470
+ Service.dotenv_set(f"{__project_name__.upper()}_LOGFIRE_ENABLED", "1")
471
+
472
+ @staticmethod
473
+ def remote_diagnostics_disable() -> None:
474
+ """Disable remote diagnostics."""
475
+ Service.dotenv_unset(f"{__project_name__.upper()}_SENTRY_ENABLED")
476
+ Service.dotenv_unset(f"{__project_name__.upper()}_LOGFIRE_ENABLED")
477
+
478
+ @staticmethod
479
+ def http_proxy_enable(
480
+ host: str,
481
+ port: int,
482
+ scheme: str,
483
+ ssl_cert_file: str | None = None,
484
+ no_ssl_verify: bool = False,
485
+ ) -> None:
486
+ """Enable HTTP proxy.
487
+
488
+ Args:
489
+ host (str): The host of the proxy server.
490
+ port (int): The port of the proxy server.
491
+ scheme (str): The scheme of the proxy server (e.g., "http", "https").
492
+ ssl_cert_file (str | None): Path to the SSL certificate file, if any.
493
+ no_ssl_verify (bool): Whether to disable SSL verification
494
+
495
+ Raises:
496
+ ValueError: If both 'ssl_cert_file' and 'ssl_disable_verify' are set.
497
+ """
498
+ url = f"{scheme}://{host}:{port}"
499
+ Service.dotenv_set("HTTP_PROXY", url)
500
+ Service.dotenv_set("HTTPS_PROXY", url)
501
+ if ssl_cert_file is not None and no_ssl_verify:
502
+ message = "Cannot set both 'ssl_cert_file' and 'ssl_disable_verify'. Please choose one."
503
+ logger.warning(message)
504
+ raise ValueError(message)
505
+ if no_ssl_verify:
506
+ Service.dotenv_set("SSL_NO_VERIFY", "1")
507
+ Service.dotenv_set("SSL_CERT_FILE", "")
508
+ Service.dotenv_set("REQUESTS_CA_BUNDLE", "")
509
+ Service.dotenv_set("CURL_CA_BUNDLE", "")
510
+ else:
511
+ Service.dotenv_unset("SSL_NO_VERIFY")
512
+ Service.dotenv_unset("SSL_CERT_FILE")
513
+ Service.dotenv_unset("REQUESTS_CA_BUNDLE")
514
+ Service.dotenv_unset("CURL_CA_BUNDLE")
515
+ if ssl_cert_file:
516
+ file = Path(ssl_cert_file).resolve()
517
+ if not file.is_file():
518
+ message = f"SSL certificate file '{ssl_cert_file}' does not exist."
519
+ logger.warning(message)
520
+ raise ValueError(message)
521
+ Service.dotenv_set("SSL_CERT_FILE", str(ssl_cert_file))
522
+ Service.dotenv_set("REQUESTS_CA_BUNDLE", str(ssl_cert_file))
523
+ Service.dotenv_set("CURL_CA_BUNDLE", str(ssl_cert_file))
524
+
525
+ @staticmethod
526
+ def http_proxy_disable() -> None:
527
+ """Disable HTTP proxy."""
528
+ Service.dotenv_unset("HTTP_PROXY")
529
+ Service.dotenv_unset("HTTPS_PROXY")
530
+ Service.dotenv_unset("SSL_CERT_FILE")
531
+ Service.dotenv_unset("SSL_NO_VERIFY")
532
+ Service.dotenv_unset("REQUESTS_CA_BUNDLE")
533
+ Service.dotenv_unset("CURL_CA_BUNDLE")
@@ -18,6 +18,7 @@ from ._constants import (
18
18
  __version__,
19
19
  )
20
20
  from ._di import load_modules, locate_implementations, locate_subclasses
21
+ from ._fs import sanitize_path, sanitize_path_component
21
22
  from ._health import Health
22
23
  from ._log import LogSettings, get_logger
23
24
  from ._logfire import LogfireSettings
@@ -59,6 +60,8 @@ __all__ = [
59
60
  "locate_implementations",
60
61
  "locate_subclasses",
61
62
  "prepare_cli",
63
+ "sanitize_path",
64
+ "sanitize_path_component",
62
65
  "strip_to_none_before_validator",
63
66
  ]
64
67
 
@@ -0,0 +1,74 @@
1
+ """Filesystem utilities."""
2
+
3
+ import platform
4
+ from pathlib import Path, PureWindowsPath
5
+
6
+ # Constants
7
+ _WIN_DRIVE_MIN_LENGTH = 2 # Minimum length for a Windows drive path (e.g. C:)
8
+
9
+
10
+ def sanitize_path_component(component: str) -> str:
11
+ """
12
+ Sanitize a single path component (e.g., filename, directory name).
13
+
14
+ Rules:
15
+ 1. On Windows: Replaces all colons with underscores (no drive letter exceptions).
16
+ 2. On non-Windows: Returns the component unchanged.
17
+
18
+ This function is useful for sanitizing individual path components like filepath.stem
19
+ where you don't want drive letter special handling.
20
+
21
+ Args:
22
+ component (str): The path component to sanitize.
23
+
24
+ Returns:
25
+ str: The sanitized path component.
26
+ """
27
+ if platform.system() == "Windows":
28
+ return component.replace(":", "_")
29
+ return component
30
+
31
+
32
+ def sanitize_path(path: str | Path, windows_replace_colon_with_underscore: bool = True) -> str | Path:
33
+ """
34
+ Sanitize a filesystem path.
35
+
36
+ Rules:
37
+ 1. If a Path is provided a Path will returned, otherwise a string will be returned.
38
+ 2. On Windows: If not disabled colons will be replaced with underscores except if it's a Windows drive letter.
39
+ 3. On Windows: If the sanitized path is reserved a ValueError will be raised.
40
+
41
+ Args:
42
+ path (str | Path): The path to sanitize.
43
+ windows_replace_colon_with_underscore (bool): if True (default) will apply rule #2
44
+
45
+ Returns:
46
+ str | Path: The sanitized path.
47
+
48
+ Raises:
49
+ ValueError: If the sanitized path is reserved on Windows.
50
+ """
51
+ is_path_object = isinstance(path, Path)
52
+ path_str = str(path)
53
+
54
+ if platform.system() == "Windows" and windows_replace_colon_with_underscore:
55
+ # Replace colons with underscores, except for Windows drive letters (e.g., C:/).
56
+ # See https://stackoverflow.com/questions/25774337/colon-in-file-names-in-python
57
+ # on NTFS creating hidden "streams" otherwise
58
+ if len(path_str) >= _WIN_DRIVE_MIN_LENGTH and path_str[1] == ":" and path_str[0].isalpha():
59
+ # Windows drive letter case - preserve drive letter, sanitize the rest
60
+ drive_part = path_str[0:2]
61
+ remaining_part = sanitize_path_component(path_str[2:])
62
+ path_str = drive_part + remaining_part
63
+ else:
64
+ # Regular case - sanitize the entire path as a component
65
+ path_str = sanitize_path_component(path_str)
66
+
67
+ if platform.system() == "Windows" and PureWindowsPath(path_str).is_reserved():
68
+ message = f"The path '{path_str}' is reserved on Windows and cannot be used. Please choose a different path."
69
+ raise ValueError(message)
70
+
71
+ # Return the same type as input
72
+ if is_path_object:
73
+ return Path(path_str)
74
+ return path_str
@@ -1,61 +0,0 @@
1
- """Homepage (index) of GUI."""
2
-
3
- from aignostics.gui import frame
4
- from aignostics.utils import BaseService, locate_subclasses
5
-
6
- from ..utils import BasePageBuilder # noqa: TID252
7
- from ._service import Service
8
-
9
-
10
- class PageBuilder(BasePageBuilder):
11
- @staticmethod
12
- def register_pages() -> None:
13
- from nicegui import run, ui # noqa: PLC0415
14
-
15
- locate_subclasses(BaseService) # Ensure settings are loaded
16
-
17
- ui.add_head_html("""
18
- <style>
19
- :global(.jse-modal-window.jse-modal-window-jsoneditor)
20
- {
21
- width: 100%;
22
- height: 100%;
23
- min-height: 900px;
24
- }
25
- </style>
26
- """)
27
-
28
- @ui.page("/system")
29
- async def page_system() -> None:
30
- """System page."""
31
- with frame("Launchpad Status", left_sidebar=False):
32
- pass
33
- ui.label("Health").classes("text-h6")
34
- properties = {
35
- "content": {"json": Service().health().model_dump()},
36
- "mode": "tree",
37
- "readOnly": True,
38
- "mainMenuBar": False,
39
- "navigationBar": False,
40
- "statusBar": False,
41
- }
42
- ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
43
-
44
- ui.label("Info").classes("text-h6")
45
- spinner = ui.spinner("dots", size="lg", color="red")
46
- properties = {
47
- "content": {"json": "Loading ..."},
48
- "mode": "tree",
49
- "readOnly": True,
50
- "mainMenuBar": False,
51
- "navigationBar": False,
52
- "statusBar": False,
53
- }
54
- editor = ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
55
- editor.set_visibility(False)
56
- info = await run.cpu_bound(Service().info, True, True)
57
- properties["content"] = {"json": info}
58
- editor.update()
59
- editor.run_editor_method(":expand", "path => true")
60
- spinner.delete()
61
- editor.set_visibility(True)
File without changes
File without changes
File without changes
File without changes