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.
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/PKG-INFO +4 -1
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/README.md +3 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/pyproject.toml +1 -1
- compox-2.1.0.dev1/src/compox/algorithm_debug.py +90 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/AlgorithmDeployer.py +15 -10
- compox-2.1.0.dev1/src/compox/algorithm_utils/LocalDebugSession.py +307 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/io_schemas.py +6 -2
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/cli.py +2 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/config/server_settings.py +3 -3
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/README.md +1 -1
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_algorithm_deployer.py +1 -1
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_server.yaml +3 -3
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/LICENSE +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/NOTICE +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/THIRD_PARTY_LICENSES.txt +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/AlgorithmConfigSchema.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/AlgorithmManager.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/BaseRunner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2AlignmentRunner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2EmbeddingRunner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2ImageRunner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2SegmentationRunner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Segmentation2SegmentationRunner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/deployment_utils.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/runner_context.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/api_builder.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/celery_builder.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/db_connection_builder.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/minio_wrapper.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/components/server_builder.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/BaseConnection.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/InMemoryConnection.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/S3Connection.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/TempfileConnection.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/database_utils.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/deploy_algorithms.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/CUDAMemoryManager.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/JobPOpen.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/ServerSystrayInterface.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/downloader.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/internal/logging.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/pydantic_models.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/resources/compoxbackend.ico +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/algorithms_controller.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/execution_controller.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/execution_manager.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/file_controller.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/file_controller_v1.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/routers/root.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/run_server.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/run_worker.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/server_utils.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/session/DataCache.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/session/TaskSession.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/session/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/DebuggingTaskHandler.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/TaskHandler.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/celery_task.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/context_task_handler.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/tasks/fastapi_background_task.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/version.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/.gitignore +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/Runner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/layer.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/layer2.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/utils.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/main.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/pyproject.toml +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/Runner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/dummy/pyproject.toml +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/Runner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/__init__.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/layer.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/layer2.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/utils.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/main.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/pyproject.toml +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/conftest.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/pytest.ini +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_base_runner.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_child_runners.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_datacache.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_delete_file.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_execute_algorithm.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_file_controller_v1.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_algorithm.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_all_algorithms.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_execution_record.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_get_file.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_post_file.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_runner_attribute_behavior.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_server_utils.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_stress.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_task_handler.py +0 -0
- {compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/test_task_session.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
self.
|
|
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(
|
|
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
|
-
|
|
771
|
-
|
|
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
|
|
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 =
|
|
65
|
-
console_port: int =
|
|
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 =
|
|
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.
|
|
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({"
|
|
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:
|
|
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:
|
|
21
|
-
console_port:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/algorithm_utils/Image2SegmentationRunner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/InMemoryConnection.py
RENAMED
|
File without changes
|
|
File without changes
|
{compox-2.0.0.dev3 → compox-2.1.0.dev1}/src/compox/database_connection/TempfileConnection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/layer.py
RENAMED
|
File without changes
|
{compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/bar/dependencies/some_more_files/layer2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/layer.py
RENAMED
|
File without changes
|
{compox-2.0.0.dev3 → compox-2.1.0.dev1}/tests/algorithms/foo/dependencies/some_more_files/layer2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|