compox 2.0.0.dev3__tar.gz → 2.1.0.dev1__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 (108) hide show
  1. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/PKG-INFO +4 -1
  2. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/README.md +3 -0
  3. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/pyproject.toml +1 -1
  4. compox-2.1.0.dev1/src/compox/algorithm_debug.py +90 -0
  5. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/AlgorithmDeployer.py +15 -10
  6. compox-2.1.0.dev1/src/compox/algorithm_utils/LocalDebugSession.py +307 -0
  7. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/io_schemas.py +6 -2
  8. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/cli.py +2 -0
  9. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/config/server_settings.py +3 -3
  10. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/README.md +1 -1
  11. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_algorithm_deployer.py +1 -1
  12. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_server.yaml +3 -3
  13. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/LICENSE +0 -0
  14. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/NOTICE +0 -0
  15. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/THIRD_PARTY_LICENSES.txt +0 -0
  16. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/__init__.py +0 -0
  17. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/AlgorithmConfigSchema.py +0 -0
  18. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/AlgorithmManager.py +0 -0
  19. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/BaseRunner.py +0 -0
  20. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2AlignmentRunner.py +0 -0
  21. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2EmbeddingRunner.py +0 -0
  22. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2ImageRunner.py +0 -0
  23. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2SegmentationRunner.py +0 -0
  24. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Segmentation2SegmentationRunner.py +0 -0
  25. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/__init__.py +0 -0
  26. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/deployment_utils.py +0 -0
  27. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/runner_context.py +0 -0
  28. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/api_builder.py +0 -0
  29. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/celery_builder.py +0 -0
  30. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/db_connection_builder.py +0 -0
  31. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/minio_wrapper.py +0 -0
  32. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/server_builder.py +0 -0
  33. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/BaseConnection.py +0 -0
  34. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/InMemoryConnection.py +0 -0
  35. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/S3Connection.py +0 -0
  36. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/TempfileConnection.py +0 -0
  37. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/__init__.py +0 -0
  38. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/database_utils.py +0 -0
  39. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/deploy_algorithms.py +0 -0
  40. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/CUDAMemoryManager.py +0 -0
  41. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/JobPOpen.py +0 -0
  42. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/ServerSystrayInterface.py +0 -0
  43. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/__init__.py +0 -0
  44. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/downloader.py +0 -0
  45. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/logging.py +0 -0
  46. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/pydantic_models.py +0 -0
  47. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/resources/compoxbackend.ico +0 -0
  48. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/__init__.py +0 -0
  49. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/algorithms_controller.py +0 -0
  50. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/execution_controller.py +0 -0
  51. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/execution_manager.py +0 -0
  52. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/file_controller.py +0 -0
  53. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/file_controller_v1.py +0 -0
  54. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/root.py +0 -0
  55. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/run_server.py +0 -0
  56. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/run_worker.py +0 -0
  57. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/server_utils.py +0 -0
  58. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/session/DataCache.py +0 -0
  59. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/session/TaskSession.py +0 -0
  60. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/session/__init__.py +0 -0
  61. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/DebuggingTaskHandler.py +0 -0
  62. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/TaskHandler.py +0 -0
  63. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/__init__.py +0 -0
  64. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/celery_task.py +0 -0
  65. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/context_task_handler.py +0 -0
  66. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/fastapi_background_task.py +0 -0
  67. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/version.py +0 -0
  68. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/.gitignore +0 -0
  69. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/__init__.py +0 -0
  70. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/Runner.py +0 -0
  71. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/__init__.py +0 -0
  72. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/__init__.py +0 -0
  73. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/layer.py +0 -0
  74. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/layer2.py +0 -0
  75. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/utils.py +0 -0
  76. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/main.py +0 -0
  77. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/pyproject.toml +0 -0
  78. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/Runner.py +0 -0
  79. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/__init__.py +0 -0
  80. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/pyproject.toml +0 -0
  81. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/Runner.py +0 -0
  82. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/__init__.py +0 -0
  83. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/__init__.py +0 -0
  84. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/__init__.py +0 -0
  85. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/layer.py +0 -0
  86. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/layer2.py +0 -0
  87. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/utils.py +0 -0
  88. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/main.py +0 -0
  89. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/pyproject.toml +0 -0
  90. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/conftest.py +0 -0
  91. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/pytest.ini +0 -0
  92. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_base_runner.py +0 -0
  93. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_child_runners.py +0 -0
  94. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_datacache.py +0 -0
  95. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_delete_file.py +0 -0
  96. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_execute_algorithm.py +0 -0
  97. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_file_controller_v1.py +0 -0
  98. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_algorithm.py +0 -0
  99. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_all_algorithms.py +0 -0
  100. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_execution_record.py +0 -0
  101. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_file.py +0 -0
  102. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_post_file.py +0 -0
  103. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_runner_attribute_behavior.py +0 -0
  104. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_server_utils.py +0 -0
  105. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_stress.py +0 -0
  106. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_task_handler.py +0 -0
  107. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_task_session.py +0 -0
  108. {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compox
3
- Version: 2.0.0.dev3
3
+ Version: 2.1.0.dev1
4
4
  Author-Email: Jan Matula <jan.matula@tescan.com>, =?utf-8?q?Old=C5=99ich_Kodym?= <oldrich.kodym@tescan.com>
5
5
  License-Expression: Apache-2.0 AND LicenseRef-Commons-Clause-1.0
6
6
  License-File: LICENSE
@@ -35,6 +35,9 @@ Description-Content-Type: text/markdown
35
35
 
36
36
  # Compox
37
37
  Compox is a simple Python execution engine for Tescan applications. It's purpose is to run data processing algorithms written in Python on the server and report the results to the client application such as Tescan 3D Viewer or Picannto.
38
+
39
+ See the [GitHub pages](https://tescan-3dim.github.io/compox) for full python API reference, docs and tutorials!
40
+
38
41
  ## Installation
39
42
  Compox can be installed through PyPI:
40
43
  ```bash
@@ -1,5 +1,8 @@
1
1
  # Compox
2
2
  Compox is a simple Python execution engine for Tescan applications. It's purpose is to run data processing algorithms written in Python on the server and report the results to the client application such as Tescan 3D Viewer or Picannto.
3
+
4
+ See the [GitHub pages](https://tescan-3dim.github.io/compox) for full python API reference, docs and tutorials!
5
+
3
6
  ## Installation
4
7
  Compox can be installed through PyPI:
5
8
  ```bash
@@ -46,7 +46,7 @@ license-files = [
46
46
  "NOTICE",
47
47
  "THIRD_PARTY_LICENSES.txt",
48
48
  ]
49
- version = "2.0.0.dev3"
49
+ version = "2.1.0.dev1"
50
50
 
51
51
  [project.scripts]
52
52
  compox = "compox.cli:app"
@@ -0,0 +1,90 @@
1
+ import json, typer
2
+ from typing import Optional, Annotated
3
+
4
+ from compox.algorithm_utils.LocalDebugSession import LocalDebugSession
5
+
6
+ app = typer.Typer(help="Commands for local debugging of algorithms")
7
+
8
+ def parse_params(value: Optional[str]) -> Optional[dict]:
9
+ """
10
+ Parse a JSON string of algorithm parameters from the command line.
11
+
12
+ Parameters
13
+ ----------
14
+ value : str | None
15
+ A JSON string containing algorithm parameters.
16
+
17
+ Returns
18
+ -------
19
+ dict | None
20
+ Parsed dictionary of parameters, or ``None`` if no input was provided.
21
+
22
+ Raises
23
+ ------
24
+ typer.BadParameter
25
+ If the string cannot be parsed as valid JSON.
26
+ """
27
+
28
+ if value is None or value == "":
29
+ return None
30
+ try:
31
+ return json.loads(value)
32
+ except json.JSONDecodeError as e:
33
+ raise typer.BadParameter(f"--params must be valid JSON: {e}")
34
+
35
+ @app.command("run")
36
+ def debug_run(
37
+ algo: str = typer.Option(".", help="Path to algorithm folder"),
38
+ data: str = typer.Option(..., help="Path to dataset folder (e.g. PNG/TIF stack)"),
39
+ device: str = typer.Option("cpu", help="Device to run on (cpu/cuda)"),
40
+ params: Annotated[Optional[str], typer.Option("--params", callback=parse_params, help="Algorithm params (JSON)")] = None,
41
+ ):
42
+ """
43
+ Run an algorithm locally in debug mode (without Compox backend).
44
+
45
+ Parameters
46
+ ----------
47
+ algo : str
48
+ Path to the algorithm directory.
49
+ data : str
50
+ Path to the folder containing image slices.
51
+ device : str
52
+ Target device to execute the algorithm on.
53
+ params : dict | None
54
+ Algorithm parameters.
55
+ """
56
+
57
+ debug(algo_dir=algo, data=data, params=params, device=device)
58
+
59
+
60
+ def debug(algo_dir=".", data=".", params=None, device="cpu"):
61
+ """
62
+ Run a local debugging session. The function initializes a `LocalDebugSession`,
63
+ loads an image stack from the specified folder, uploads it into the temporary
64
+ database, and executes the algorithm in the given directory.
65
+
66
+ Parameters
67
+ ----------
68
+ algo_dir : str, optional
69
+ Path to the algorithm folder. Default is the current directory (".").
70
+ data : str, optional
71
+ Path to the dataset folder (e.g. a directory with PNG/TIF slices).
72
+ params : dict, optional
73
+ Dictionary of algorithm parameters.
74
+ device : str, optional
75
+ Target device for execution. Default is "cpu".
76
+
77
+ Returns
78
+ -------
79
+ Any
80
+ Output returned by the algorithm’s ``run()`` method.
81
+ """
82
+
83
+ sess = LocalDebugSession(device=device)
84
+ input_ids = sess.load_data(data)
85
+ out = sess.run(algo_dir, {"input_dataset_ids": input_ids}, args=params or {})
86
+ print(out)
87
+ return out
88
+
89
+ if __name__ == "__main__":
90
+ app()
@@ -11,6 +11,7 @@ import shutil
11
11
  import glob
12
12
  import tempfile
13
13
  import zipimport
14
+ import importlib
14
15
  import sys
15
16
  import re
16
17
  import hashlib
@@ -99,12 +100,10 @@ class AlgorithmDeployer:
99
100
  "additional_parameters"
100
101
  ]
101
102
 
102
- self.check_importable = pyproject_toml["tool"]["compox"][
103
- "check_importable"
104
- ]
105
- self.obfuscate = pyproject_toml["tool"]["compox"]["obfuscate"]
106
- self.hash_module = pyproject_toml["tool"]["compox"]["hash_module"]
107
- self.hash_assets = pyproject_toml["tool"]["compox"]["hash_assets"]
103
+ self.check_importable = pyproject_toml.get("tool", {}).get("compox", {}).get("check_importable", False)
104
+ self.obfuscate = pyproject_toml.get("tool", {}).get("compox", {}).get("obfuscate", True)
105
+ self.hash_module = pyproject_toml.get("tool", {}).get("compox", {}).get("hash_module", True)
106
+ self.hash_assets = pyproject_toml.get("tool", {}).get("compox", {}).get("hash_assets", True)
108
107
 
109
108
  def parse_pyproject_toml(self, path_to_algorithm_directory: str) -> dict:
110
109
  """
@@ -382,7 +381,9 @@ class AlgorithmDeployer:
382
381
  module_path + ".zip"
383
382
  )
384
383
  if not importable:
385
- raise ValueError("The runner cannot be imported.")
384
+ raise ValueError(
385
+ "The current environment cannot import the the algorithm module. Check the above logs for more details about the ImportError cause. This check can be disabled by setting check_importable to False in the affected algorithm's pyproject.toml file."
386
+ )
386
387
 
387
388
  zip_bytes = open(module_path + ".zip", "rb").read()
388
389
  # store the zip file in the module-store collection
@@ -767,10 +768,14 @@ class AlgorithmDeployer:
767
768
  """
768
769
  try:
769
770
  sys.path.insert(0, path_to_zip)
770
- module = zipimport.zipimporter(path_to_zip)
771
- module.load_module("Runner")
771
+ importer = zipimport.zipimporter(path_to_zip)
772
+ spec = importlib.util.spec_from_loader("Runner", importer)
773
+ if spec is not None:
774
+ module = importlib.util.module_from_spec(spec)
775
+ importer.exec_module(module)
772
776
  return True
773
- except Exception as e:
777
+ except ImportError as e:
778
+ logger.error(f"ImportError: {e}")
774
779
  return False
775
780
 
776
781
  @staticmethod
@@ -0,0 +1,307 @@
1
+ import os, glob, io, uuid, h5py
2
+ import numpy as np
3
+ from PIL import Image, ImageSequence
4
+
5
+ from compox.tasks.DebuggingTaskHandler import DebuggingTaskHandler
6
+
7
+ class LocalDebugSession:
8
+ """
9
+ Helper class for running Compox algorithms locally in a simplified
10
+ "debug" environment.
11
+
12
+ It sets up a fake `TaskHandler`, uploads image data to an in-memory database,
13
+ and runs the specified algorithm. It allows developers to test and debug
14
+ algorithms without deploying them to a full Compox server.
15
+
16
+ Parameters
17
+ ----------
18
+ task_id : str, optional
19
+ Unique ID for the debugging task. Default is `"local-debug"`.
20
+ device : str, optional
21
+ Target device to run the algorithm on. Default is `"cpu"`.
22
+ tmp_bucket : str, optional
23
+ Temporary in-memory storage bucket name for the mock database.
24
+ Default is `"data-store"`.
25
+ """
26
+
27
+ def __init__(self, task_id="local-debug", device="cpu", tmp_bucket="data-store"):
28
+ self.task = DebuggingTaskHandler(task_id)
29
+ self.task.set_as_current_task_handler()
30
+ self.bucket = tmp_bucket
31
+ self.device = device
32
+
33
+
34
+ def _to_h5_bytes(self, arr):
35
+ """
36
+ Convert a NumPy array into an in-memory HDF5 file. Each image slice
37
+ is stored as a small binary HDF5 object (byte array), which mimics
38
+ the format expected by the Compox database.
39
+
40
+ Parameters
41
+ ----------
42
+ arr : np.ndarray
43
+ Image array to be serialized into HDF5 format.
44
+
45
+ Returns
46
+ -------
47
+ bytes
48
+ The in-memory representation of the HDF5 file.
49
+ """
50
+
51
+ bio = io.BytesIO()
52
+ with h5py.File(bio, "w") as f:
53
+ f.create_dataset("image", data=arr)
54
+ return bio.getvalue()
55
+
56
+
57
+ def load_data(self, source):
58
+ """
59
+ Load image data from a specified source into the local debug database.
60
+
61
+ Parameters
62
+ ----------
63
+ source : str
64
+ Path to the data source. Can be a folder containing image slices,
65
+ a single image file (PNG/JPEG/TIFF), a .npy file, or an HDF5 file.
66
+
67
+ Returns
68
+ -------
69
+ list[str]
70
+ A list of generated object IDs (UUIDs) corresponding to the stored
71
+ HDF5 image slices.
72
+ """
73
+ if os.path.isdir(source):
74
+ return self.load_from_folder(source)
75
+ else:
76
+ ext = os.path.splitext(source)[1].lower()
77
+ if ext in (".tif", ".tiff"):
78
+ return self.load_from_tiff(source)
79
+ elif ext in (".png", ".jpg", ".jpeg"):
80
+ return self.load_from_pil_image(source)
81
+ elif ext == ".npy":
82
+ return self.load_from_npy(source)
83
+ elif ext in (".h5", ".hdf5"):
84
+ return self.load_from_h5(source)
85
+ else:
86
+ raise ValueError(f"Unsupported file type: {ext}")
87
+
88
+
89
+ def load_from_npy(self, filepath: str) -> list[str]:
90
+ """
91
+ Load a .npy file and store it as individual 2D slices in the fake database.
92
+
93
+ Parameters
94
+ ----------
95
+ filepath : str
96
+ Path to the .npy file containing a 2D or 3D NumPy array.
97
+
98
+ Returns
99
+ -------
100
+ list[str]
101
+ A list of generated object IDs (UUIDs) corresponding to the stored
102
+ HDF5 image slices.
103
+ """
104
+ arr = np.load(filepath)
105
+ nd = arr.ndim
106
+
107
+ slices_bytes: list[bytes] = []
108
+
109
+ if nd == 3:
110
+ for z in range(arr.shape[0]):
111
+ slices_bytes.append(self._to_h5_bytes(arr[z]))
112
+ elif nd == 2:
113
+ slices_bytes.append(self._to_h5_bytes(arr))
114
+ else:
115
+ raise ValueError(
116
+ f"Unsupported numpy array ndim={nd} in '{filepath}'. "
117
+ "Expected 2D (H, W) or 3D (Z, H, W)."
118
+ )
119
+
120
+ ids = [str(uuid.uuid4()) for _ in slices_bytes]
121
+ self.task.database_connection.put_objects(self.bucket, ids, slices_bytes)
122
+ return ids
123
+
124
+
125
+ def load_from_h5(self, filepath: str) -> list[str]:
126
+ """
127
+ Load a 2D or 3D dataset from an HDF5 file and store it as 2D slices.
128
+
129
+ Parameters
130
+ ----------
131
+ filepath : str
132
+ Path to the .h5/.hdf5 file.
133
+
134
+ Returns
135
+ -------
136
+ list[str]
137
+ List of object IDs corresponding to stored slices.
138
+
139
+ Raises
140
+ ------
141
+ KeyError
142
+ If no suitable 2D/3D dataset can be found in the file.
143
+ ValueError
144
+ If the selected dataset has unsupported number of dimensions.
145
+ """
146
+ slices_bytes: list[bytes] = []
147
+
148
+ def _find_datasets(f):
149
+ found = []
150
+ def visitor(name, obj):
151
+ if isinstance(obj, h5py.Dataset) and obj.ndim in (2, 3):
152
+ found.append((name, obj))
153
+ f.visititems(visitor)
154
+ return found
155
+
156
+ with h5py.File(filepath, "r") as f:
157
+
158
+ datasets = _find_datasets(f)
159
+
160
+ if not datasets:
161
+ raise KeyError(
162
+ f"No suitable 2D/3D dataset found anywhere in '{filepath}'. "
163
+ f"Available top-level groups: {list(f.keys())}"
164
+ )
165
+
166
+ _, dset = datasets[0]
167
+
168
+ if dset.ndim == 2:
169
+ slices_bytes.append(self._to_h5_bytes(dset[()]))
170
+
171
+ elif dset.ndim == 3:
172
+ for z in range(dset.shape[0]):
173
+ slices_bytes.append(self._to_h5_bytes(dset[z, ...]))
174
+ else:
175
+ raise ValueError("Internal error: filtered ndim != 2/3")
176
+
177
+ ids = [str(uuid.uuid4()) for _ in slices_bytes]
178
+ self.task.database_connection.put_objects(self.bucket, ids, slices_bytes)
179
+ return ids
180
+
181
+
182
+ def load_from_pil_image(self, filepath: str) -> list[str]:
183
+ """
184
+ Load a single image file (PNG/JPEG/...) and store it as one 2D slice
185
+ in the debug database.
186
+
187
+ Parameters
188
+ ----------
189
+ filepath : str
190
+ Path to the image file.
191
+
192
+ Returns
193
+ -------
194
+ list[str]
195
+ List with a single object ID corresponding to the stored slice.
196
+ """
197
+ with Image.open(filepath) as img:
198
+ arr = np.asarray(img)
199
+
200
+ slice_bytes = self._to_h5_bytes(arr)
201
+ obj_id = str(uuid.uuid4())
202
+
203
+ self.task.database_connection.put_objects(self.bucket, [obj_id], [slice_bytes])
204
+ return [obj_id]
205
+
206
+ def load_from_tiff(self, filepath: str) -> list[str]:
207
+ """
208
+ Load a TIFF file (single-page or multi-page) and store each page
209
+ as a separate 2D slice in the fake database.
210
+
211
+ Parameters
212
+ ----------
213
+ filepath : str
214
+ Path to the .tif/.tiff file.
215
+
216
+ Returns
217
+ -------
218
+ list[str]
219
+ List of object IDs corresponding to stored slices.
220
+ """
221
+ slices_bytes: list[bytes] = []
222
+
223
+ with Image.open(filepath) as im:
224
+ frames = list(ImageSequence.Iterator(im))
225
+
226
+ if not frames:
227
+ raise ValueError(f"TIFF file '{filepath}' contains no frames.")
228
+
229
+ for frame in frames:
230
+ arr = np.asarray(frame)
231
+ slices_bytes.append(self._to_h5_bytes(arr))
232
+
233
+ ids = [str(uuid.uuid4()) for _ in slices_bytes]
234
+ self.task.database_connection.put_objects(self.bucket, ids, slices_bytes)
235
+ return ids
236
+
237
+
238
+ def load_from_folder(self, folder, exts=(".tif", ".tiff", ".png", ".jpg", ".jpeg")):
239
+ """
240
+ Load a folder of image slices (e.g., PNG/TIFF) into the local database.
241
+ Each image is read from disk, converted to a NumPy array, serialized
242
+ into an HDF5 byte object, and uploaded into the fake database.
243
+
244
+ Parameters
245
+ ----------
246
+ folder : str
247
+ Path to the folder containing image slices.
248
+ exts : tuple of str, optional
249
+ Allowed image extensions. Default is `(".tif", ".tiff", ".png", ".jpg", ".jpeg")`.
250
+
251
+ Returns
252
+ -------
253
+ list[str]
254
+ A list of generated object IDs (UUIDs) corresponding to the stored
255
+ HDF5 image slices.
256
+
257
+ Raises
258
+ ------
259
+ ValueError
260
+ If no supported image slices are found in the folder.
261
+ """
262
+
263
+ paths = [p for p in sorted(glob.glob(os.path.join(folder, "*"))) if p.lower().endswith(exts)]
264
+ if not paths:
265
+ raise ValueError(
266
+ f"No supported image slices found in folder '{folder}'. "
267
+ f"Supported extensions: {exts}"
268
+ )
269
+ ext = os.path.splitext(paths[0])[1].lower()
270
+ if ext not in exts:
271
+ raise ValueError(
272
+ f"Folder loader expects slice image formats {exts}, "
273
+ f"but found '{ext}' in folder: {folder}. "
274
+ "If you want to load numpy/HDF5 volumes, pass the file path directly "
275
+ "instead of a folder."
276
+ )
277
+ files = []
278
+ for p in paths:
279
+ with Image.open(p) as img:
280
+ files.append(self._to_h5_bytes(np.asarray(img)))
281
+ ids = [str(uuid.uuid4()) for _ in files]
282
+ self.task.database_connection.put_objects(self.bucket, ids, files)
283
+ return ids
284
+
285
+
286
+ def run(self, algo_dir, inputs: dict, args: dict | None = None):
287
+ """
288
+ Execute a Compox algorithm locally. This method fetches and runs the algorithm
289
+ from the specified folder.
290
+
291
+ Parameters
292
+ ----------
293
+ algo_dir : str
294
+ Path to the directory containing the algorithm implementation.
295
+ inputs : dict
296
+ Input metadata for the algorithm (e.g. dataset IDs).
297
+ args : dict, optional
298
+ Algorithm parameters passed as keyword arguments.
299
+
300
+ Returns
301
+ -------
302
+ Any
303
+ The algorithm's output object, as returned by its `run()` method.
304
+ """
305
+
306
+ runner = self.task.fetch_algorithm(algo_dir, device=self.device)
307
+ return runner.run(inputs, args=args or {})
@@ -12,6 +12,10 @@ class DataSchema(BaseModel):
12
12
  model_config = ConfigDict(arbitrary_types_allowed=True)
13
13
 
14
14
 
15
+ class GenericSchema(DataSchema):
16
+ data: np.ndarray
17
+
18
+
15
19
  class SegmentationSchema(DataSchema):
16
20
  mask: np.ndarray
17
21
 
@@ -28,9 +32,9 @@ class ImageSchema(DataSchema):
28
32
  raise ValueError(
29
33
  f"Image must be 2D (got {v.ndim} dimensions), grayscale or RGB (got {1 if v.ndim == 2 else v.shape[0]} channels)."
30
34
  )
31
- if v.dtype not in [np.uint8, np.uint16, np.float32, np.float16]:
35
+ if v.dtype not in [np.uint8, np.uint16, np.float64, np.float32, np.float16]:
32
36
  raise ValueError(
33
- f"Image must be uint8, uint16, float32 or float16 (got {v.dtype})."
37
+ f"Image must be uint8, uint16, float64, float32 or float16 (got {v.dtype})."
34
38
  )
35
39
  return v
36
40
 
@@ -11,11 +11,13 @@ import subprocess
11
11
  import os
12
12
  from typing import Optional
13
13
  from compox.config.server_settings import Settings
14
+ from compox.algorithm_debug import app as debug_app
14
15
 
15
16
  app = typer.Typer(
16
17
  help="This CLI tool contains commands for running the Compox."
17
18
  )
18
19
 
20
+ app.add_typer(debug_app, name="debug")
19
21
 
20
22
  @app.command(
21
23
  "run",
@@ -61,8 +61,8 @@ class MinioSettings(BaseModel):
61
61
  model_config = ConfigDict(extra='forbid')
62
62
  provider: Literal["minio"] = "minio"
63
63
  start_instance: bool = True
64
- port: int = 9091
65
- console_port: int = 9090
64
+ port: int = 5483
65
+ console_port: int = 5482
66
66
  executable_path: str = os.path.join("minio", "minio_bin") if os.name != "nt" else os.path.join("minio", "minio.exe")
67
67
  storage_path: str = os.path.join("minio", "compox_store")
68
68
  # derived attributes
@@ -186,7 +186,7 @@ class Settings(BaseSettings, cli_parse_args=_PARSE_CLI):
186
186
 
187
187
  model_config = ConfigDict(extra='forbid', env_nested_delimiter='__', env_prefix="COMPOX_")
188
188
 
189
- port: int = 5461
189
+ port: int = 5481
190
190
  deploy_algorithms_from: str = "./algorithms" # points to default foo/bar algorithms
191
191
 
192
192
  info: CompoxInfo = CompoxInfo()
@@ -56,7 +56,7 @@ additional_parameters = [
56
56
  ]
57
57
  ```
58
58
 
59
- The `check_importable` field is used to check if the algorithm can be imported. If set to `true`, compox will check if the algorithm can be imported before deploying it as a service. (NOTE: THIS CURRENTLY DOES NOT WORK).
59
+ The `check_importable` field is used to check if the algorithm can be imported. If set to `true`, compox will check if the algorithm can be imported before deploying it as a service.
60
60
 
61
61
  ```toml
62
62
  check_importable = false
@@ -74,7 +74,7 @@ def invalid_alg_dir(tmp_path):
74
74
  d = tmp_path / "invalid"
75
75
  d.mkdir()
76
76
  (d / "pyproject.toml").write_text(
77
- toml.dumps({"project": {"name": "foo", "version": "0.1"}})
77
+ toml.dumps({"some_invalid_section": {"foo": "bar"}})
78
78
  )
79
79
  (d / "Runner.py").write_text("class Runner: pass")
80
80
  return str(d)
@@ -1,4 +1,4 @@
1
- port: 5461
1
+ port: 5484
2
2
  deploy_algorithms_from: "./algorithms"
3
3
 
4
4
  info:
@@ -17,8 +17,8 @@ storage:
17
17
  backend_settings:
18
18
  provider: "minio"
19
19
  start_instance: true
20
- port: 9091
21
- console_port: 9090
20
+ port: 5486
21
+ console_port: 5485
22
22
  executable_path: "minio/TESCAN3DBackendDB.exe"
23
23
  storage_path: "minio/compox_store"
24
24
 
File without changes
File without changes