diff --git a/.gitignore b/.gitignore index ed8ebf583f771da9150c35db3955987b7d757904..5363ae7c15dd829079a8bf8594b3a15389393041 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -__pycache__ \ No newline at end of file +__pycache__ +.ruff_cache +.hypothesis +.venv +dist diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5c0439e214c839d0d65c47e5e6726579a4dfe8cc..14bed04f307d271c77c094bee9500edfc15f1472 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ image: ubuntu:latest stages: - code_embed + - ruff variables: GIT_STRATEGY: clone @@ -9,7 +10,6 @@ variables: code_embedder: stage: code_embed script: - - echo "Personal a" - cd code-embedder/ - poetry install --only main - README_PATHS=$(find .. -maxdepth 1 -name "*.md" -print) @@ -40,3 +40,13 @@ code_embedder: - merge_requests +ruff_linter: + stage: ruff + script: + - curl -LsSf https://astral.sh/uv/install.sh | sh + - source $HOME/.local/bin/env + - uv tool run ruff check + before_script: + - apt-get update && apt-get install -y curl + only: + - merge_requests diff --git a/README.md b/README.md index c9c3548abb373c4db0ec3f7e1b06528196bc5f65..8473b81d5c4bad23619df9f3d7a9ccb9a603cc9b 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,17 @@ from thd_json.projection import get_projection_validator from pathlib import Path -FILE = Path('./examples/thd_json/test_thd.json') +FILE = Path("./examples/thd_json/test_thd.json") def main(): projection_validator = get_projection_validator() projection_validator.file(FILE) -if __name__ == '__main__': + +if __name__ == "__main__": main() + ``` Python example to validate a folder of projections @@ -92,31 +94,36 @@ from pathlib import Path import argparse -def get_projection_validator(json_suffix: str = '*.json') -> Validator: - return Validator(Path(__file__).parent / Path('projection.json'), json_suffix) +def get_projection_validator(json_suffix: str = "*.json") -> Validator: + return Validator(Path(__file__).parent / Path("projection.json"), json_suffix) -def projection_cli(): - parser = argparse.ArgumentParser(description='JSON THD Projection Validator CLI with uv.') - parser.add_argument('folder', help='Folder to check.', type=str) - parser.add_argument('suffix', help='Projection suffix.', default='*.json', type=Path, nargs='?') + +def projection_geometry_cli(): + parser = argparse.ArgumentParser( + description="JSON THD Projection Geometry Validator CLI with uv." + ) + parser.add_argument("folder", help="Folder to check.", type=str) + parser.add_argument( + "suffix", help="Projection suffix.", default="*.json", type=Path, nargs="?" + ) args = parser.parse_args() suffix = str(args.suffix) - if not suffix.startswith('*'): + if not suffix.startswith("*"): raise ValueError(f'The suffix must always start with: "*". \nIt is: {suffix}') - validator = get_projection_validator( - suffix) - + validator = get_projection_validator(suffix) + folder = Path(args.folder) if not folder.exists(): - raise FileNotFoundError(f'Folder: {folder} does not exist') + raise FileNotFoundError(f"Folder: {folder} does not exist") validator.folder(Path(args.folder)) if __name__ == "__main__": - projection_cli() + projection_geometry_cli() + ``` - add the CLI script to the `pyproject.toml` ```toml diff --git a/examples/artist/projection.json b/examples/artist/projection.json index 9e4637a3c68b109ba53b823bf73255fa884c6818..9c04612b6cd3c4a40ccd4c4941745ded1f45ebb0 100644 --- a/examples/artist/projection.json +++ b/examples/artist/projection.json @@ -1,28 +1,43 @@ { - "focal_spot_position_mm": [ - -10.0, - -10.0, - -10.0 - ], - "focal_spot_orientation_quat": [ - 0.0, - 0.0, - 0.0, - 1.0 - ], - "detector_center_position_mm": [ - 990.0, - -10.0, - -10.0 - ], - "detector_center_orientation_quat": [ - 0.0, - 0.7071067811865475, - 0.0, - 0.7071067811865477 - ], - "image_width_px": 2000, - "pixel_pitch_width_mm": 0.13899999856948853, - "image_height_px": 1800, - "pixel_pitch_height_mm": 0.13899999856948853 + "header": + { + "uuid": "b83d81a1-333f-448e-af75-7c7db317fd96", + "timestamp": "2018-11-13T20:20:39+00:00" + }, + "image": + { + "uuid": "b83d81a1-333f-448e-af75-7c7db317fd96", + "timestamp": "2018-11-13T20:20:39+00:00", + "image_path": "test_thd.tif" + }, + "projection_geometry":{ + "focal_spot_position_mm": [ + -10.0, + -10.0, + -10.0 + ], + "focal_spot_orientation_quat": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "detector_center_position_mm": [ + 990.0, + -10.0, + -10.0 + ], + "detector_center_orientation_quat": [ + 0.0, + 0.7071067811865475, + 0.0, + 0.7071067811865477 + ] + }, + "detector": { + "image_width_px": 2000, + "pixel_pitch_width_mm": 0.13899999856948853, + "image_height_px": 1800, + "pixel_pitch_height_mm": 0.13899999856948853 + } } \ No newline at end of file diff --git a/examples/generated/example_detector.json b/examples/generated/example_detector.json new file mode 100644 index 0000000000000000000000000000000000000000..7708891bcd11793d633b3d0f2e82d069eb735b97 --- /dev/null +++ b/examples/generated/example_detector.json @@ -0,0 +1,6 @@ +{ + "image_height_px": 1, + "image_width_px": 1, + "pixel_pitch_height_mm": 0.001, + "pixel_pitch_width_mm": 0.001 +} \ No newline at end of file diff --git a/examples/generated/example_header.json b/examples/generated/example_header.json new file mode 100644 index 0000000000000000000000000000000000000000..4fbfdd4f1a2e12fcfde73a313b596ab3131a2e00 --- /dev/null +++ b/examples/generated/example_header.json @@ -0,0 +1,4 @@ +{ + "timestamp": "2000-01-01T00:00:00Z", + "uuid": "" +} \ No newline at end of file diff --git a/examples/generated/example_image.json b/examples/generated/example_image.json new file mode 100644 index 0000000000000000000000000000000000000000..5f601cdb514c614546fc8658bec1f74d8220bb7c --- /dev/null +++ b/examples/generated/example_image.json @@ -0,0 +1,5 @@ +{ + "image_path": ".tif", + "timestamp": "2000-01-01T00:00:00Z", + "uuid": "" +} \ No newline at end of file diff --git a/examples/generated/example_projection.json b/examples/generated/example_projection.json new file mode 100644 index 0000000000000000000000000000000000000000..e6f19765015ef42bc11483de9bb2b77b0e79d5ad --- /dev/null +++ b/examples/generated/example_projection.json @@ -0,0 +1,35 @@ +{ + "detector": { + "image_height_px": 1, + "image_width_px": 1, + "pixel_pitch_height_mm": 0.001, + "pixel_pitch_width_mm": 0.001 + }, + "header": { + "timestamp": "2000-01-01T00:00:00Z", + "uuid": "" + }, + "image": { + "image_path": ".tif", + "timestamp": "2000-01-01T00:00:00Z", + "uuid": "" + }, + "projection_geometry": { + "detector_center_orientation_quat": [ + 0.0, + 0.0, + 0.0, + 0.0 + ], + "detector_center_position_mm": [ + 0.0, + 0.0, + 0.0 + ], + "focal_spot_position_mm": [ + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/examples/generated/example_source.json b/examples/generated/example_source.json new file mode 100644 index 0000000000000000000000000000000000000000..206d614fcfa2963362528768937dd219d46541e2 --- /dev/null +++ b/examples/generated/example_source.json @@ -0,0 +1,4 @@ +{ + "current_ma": 0.001, + "voltage_kv": 0.001 +} \ No newline at end of file diff --git a/examples/thd_json/test_thd.json b/examples/thd_json/test_thd.json index 0a345d279d73bf3babac630e30714a6128db9b3c..d598300fe04037cfcb3d1d0c1204aec6df7b022e 100644 --- a/examples/thd_json/test_thd.json +++ b/examples/thd_json/test_thd.json @@ -4,54 +4,44 @@ "uuid": "b83d81a1-333f-448e-af75-7c7db317fd96", "timestamp": "2018-11-13T20:20:39+00:00" }, - "focal_spot_position_mm": [ - 2724.71682, - 1690.5, - 193.29345 - ], - "detector_center_position_mm": [ - 1902.85619, - 1690.5, - 121.38996 - ], - "detector_horizontal_vector": [ - -0.08715574274765814, - 0.0, - 0.9961946980917458 - ], - "detector_vertical_vector": [ - -0.0, - -1.0000000000000002, - 0.0 - ], - "detector_center_orientation_quat": [ - 0.6755902076156602, - -0.0, - 0.737277336810124, - 0.0 - ], - "pixel_pitch_width_mm": 0.07479989711934157, - "pixel_pitch_height_mm": 0.07479989711934157, - "image_width_px": 1944, - "image_height_px": 1536, - "projection_matrix_cera": [ - [ - -0.17494827015512399, - 0.0, - 0.988513847201303, - 285.61124242325764 + "image": + { + "uuid": "b83d81a1-333f-448e-af75-7c7db317fd96", + "timestamp": "2018-11-13T20:20:39+00:00", + "image_path": "test_thd.tif" + }, + "projection_geometry": { + "focal_spot_position_mm": [ + 2724.71682, + 1690.5, + 193.29345 ], - [ - -0.06936693345179354, - -0.9999999999999998, - -0.006068820300589123, - 1880.678313541253 + "detector_center_position_mm": [ + 1902.85619, + 1690.5, + 121.38996 ], - [ - -9.032152793202284e-05, + "detector_horizontal_vector": [ + -0.08715574274765814, 0.0, - -7.902109766392087e-06, - 0.24762801242350704 + 0.9961946980917458 + ], + "detector_vertical_vector": [ + -0.0, + -1.0000000000000002, + 0.0 + ], + "detector_center_orientation_quat": [ + 0.6755902076156602, + -0.0, + 0.737277336810124, + 0.0 ] - ] + }, + "detector": { + "pixel_pitch_width_mm": 0.07479989711934157, + "pixel_pitch_height_mm": 0.07479989711934157, + "image_width_px": 1944, + "image_height_px": 1536 + } } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5dda9732d721e27cdd86f11c45e5a7fb453dd5b1..28157f8d23169d6f50f97a289b7bd8456514e9ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,8 @@ authors = [ ] requires-python = ">=3.10" dependencies = [ + "hypothesis>=6.119.0", + "hypothesis-jsonschema>=0.23.1", "json-schema>=0.3", "jsonschema>=4.23.0", ] @@ -14,6 +16,11 @@ dependencies = [ [project.scripts] projection_validator = "thd_json.projection:projection_cli" header_validator = "thd_json.header:header_cli" +generate_header = "thd_json.header:generate_header_cli" +image_validator = "thd_json.image:image_cli" +detector_validator = "thd_json.detector:detector_cli" +source_validator = "thd_json.source:source_cli" +projection_geometry_validator = "thd_json.projection_geometry:projection_geometry_cli" [build-system] requires = ["hatchling"] diff --git a/scripts/generate_example_data.py b/scripts/generate_example_data.py new file mode 100644 index 0000000000000000000000000000000000000000..9dc224031186f916cd547e534a9ae9c357c6cfe6 --- /dev/null +++ b/scripts/generate_example_data.py @@ -0,0 +1,38 @@ +from hypothesis_jsonschema import from_schema +from thd_json.projection import get_projection_validator +from thd_json.header import get_header_validator +from thd_json.source import get_source_validator +from thd_json.detector import get_detector_validator +from thd_json.image import get_image_validator +from hypothesis import given, settings +import json +from pathlib import Path + + +SAVE_FOLDER = Path("./examples/generated") + + +def main(): + for name, get_validator in [ + ("projection", get_projection_validator), + ("header", get_header_validator), + ("source", get_source_validator), + ("detector", get_detector_validator), + ("image", get_image_validator), + ]: + validator = get_validator() + schema = validator.get_schema() + strategy = from_schema(schema) + + @settings(max_examples=1) + @given(strategy) + def schema(data): + with open(SAVE_FOLDER / f"example_{name}.json", "w") as f: + json.dump(data, f, indent=4) + return + + schema() + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_file.py b/scripts/validate_file.py index dc79c7b6a511eb4d19dcd7e043113bc396f282fa..2b27d3a4eb32dcc8320a809e16a756fd6f218711 100644 --- a/scripts/validate_file.py +++ b/scripts/validate_file.py @@ -2,12 +2,13 @@ from thd_json.projection import get_projection_validator from pathlib import Path -FILE = Path('./examples/thd_json/test_thd.json') +FILE = Path("./examples/thd_json/test_thd.json") def main(): projection_validator = get_projection_validator() projection_validator.file(FILE) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/scripts/validate_folder.py b/scripts/validate_folder.py index 0813bac1a93a79e13b63b482a45e8ecc5d7b554c..9517ec1b892dfb0a4496e387d4dbe5260af62ef8 100644 --- a/scripts/validate_folder.py +++ b/scripts/validate_folder.py @@ -2,12 +2,13 @@ from thd_json.projection import get_projection_validator from pathlib import Path -FOLDER = Path('./examples/artist') +FOLDER = Path("./examples/artist") def main(): projection_validator = get_projection_validator() projection_validator.folder(FOLDER) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/src/thd_json/__init__.py b/src/thd_json/__init__.py index a95ecf83bfaa301dc2a1b9ff66d8032e43e852c5..9e05c3d0675746031424bac9f9e8c5fff21e0c4b 100644 --- a/src/thd_json/__init__.py +++ b/src/thd_json/__init__.py @@ -1 +1,3 @@ -from .validation import Validator \ No newline at end of file +from .validation import Validator + +__all__ = ["Validator"] diff --git a/src/thd_json/detector/__init__.py b/src/thd_json/detector/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0ca65f8876de6c3d4b67503c3d12f2434f00b693 --- /dev/null +++ b/src/thd_json/detector/__init__.py @@ -0,0 +1,35 @@ +from thd_json import Validator + +from pathlib import Path +import argparse + + +def get_detector_validator(json_suffix: str = "*.json") -> Validator: + return Validator(Path(__file__).parent / Path("detector.json"), json_suffix) + + +def detector_cli(): + parser = argparse.ArgumentParser( + description="JSON THD Detector Validator CLI with uv." + ) + parser.add_argument("folder", help="Folder to check.", type=str) + parser.add_argument( + "suffix", help="Projection suffix.", default="*.json", type=Path, nargs="?" + ) + args = parser.parse_args() + + suffix = str(args.suffix) + if not suffix.startswith("*"): + raise ValueError(f'The suffix must always start with: "*". \nIt is: {suffix}') + + validator = get_detector_validator(suffix) + + folder = Path(args.folder) + if not folder.exists(): + raise FileNotFoundError(f"Folder: {folder} does not exist") + + validator.folder(Path(args.folder)) + + +if __name__ == "__main__": + detector_cli() diff --git a/src/thd_json/detector/detector.json b/src/thd_json/detector/detector.json new file mode 100644 index 0000000000000000000000000000000000000000..14c555455f34fc34e1b87b1fc285f6c9cbf07bf2 --- /dev/null +++ b/src/thd_json/detector/detector.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "THD Detector File Schema", + "type": "object", + "version": 0.6, + "date": "19.11.2024", + "properties": { + "pixel_pitch_width_mm": { + "type": "number", + "description": "Pixel pitch in width dimension measured in millimeters.", + "minimum": 0.001 + }, + "pixel_pitch_height_mm": { + "type": "number", + "description": "Pixel pitch in height dimension measured in millimeters.", + "minimum": 0.001 + }, + "image_width_px": { + "type": "integer", + "description": "Width of the image in pixels.", + "minimum": 1 + }, + "image_height_px": { + "type": "integer", + "description": "Height of the image in pixels.", + "minimum": 1 + } + }, + "required": [ + "pixel_pitch_height_mm", + "pixel_pitch_width_mm", + "image_width_px", + "image_height_px" + ], + "additionalProperties": false + } + \ No newline at end of file diff --git a/src/thd_json/header/__init__.py b/src/thd_json/header/__init__.py index fcfc2d07132bd9d84ef0c1789896de554414a88f..4af07ce236d9cbf1f189f46cd4b2b77eb862c2b1 100644 --- a/src/thd_json/header/__init__.py +++ b/src/thd_json/header/__init__.py @@ -3,29 +3,55 @@ from thd_json import Validator from pathlib import Path import argparse +from dataclasses import dataclass +import uuid +from datetime import datetime, timedelta + + +@dataclass +class JsonHeader: + uuid: str + timestamp: str + + +def get_header_validator(json_suffix: str = "*.json") -> Validator: + return Validator(Path(__file__).parent / Path("header.json"), json_suffix) -def get_header_validator(json_suffix: str = '*.json') -> Validator: - return Validator(Path(__file__).parent / Path('header.json'), json_suffix) def header_cli(): - parser = argparse.ArgumentParser(description='JSON THD Header Validator CLI with uv.') - parser.add_argument('folder', help='Folder to check.', type=str) - parser.add_argument('suffix', help='Projection suffix.', default='*.json', type=Path, nargs='?') + parser = argparse.ArgumentParser( + description="JSON THD Header Validator CLI with uv." + ) + parser.add_argument("folder", help="Folder to check.", type=str) + parser.add_argument( + "suffix", help="Projection suffix.", default="*.json", type=Path, nargs="?" + ) args = parser.parse_args() suffix = str(args.suffix) - if not suffix.startswith('*'): + if not suffix.startswith("*"): raise ValueError(f'The suffix must always start with: "*". \nIt is: {suffix}') - validator = get_header_validator( - suffix) - + validator = get_header_validator(suffix) + folder = Path(args.folder) if not folder.exists(): - raise FileNotFoundError(f'Folder: {folder} does not exist') + raise FileNotFoundError(f"Folder: {folder} does not exist") validator.folder(Path(args.folder)) +def generate_header() -> JsonHeader: + generated_uuid = uuid.uuid1() + timestamp = (generated_uuid.time - 0x01B21DD213814000) / 1e7 + datetime_from_uuid = datetime(1970, 1, 1) + timedelta(seconds=timestamp) + return JsonHeader(str(generated_uuid), str(datetime_from_uuid)) + + +def generate_header_cli(): + header = generate_header() + print(f"{header.uuid};{header.timestamp}") + + if __name__ == "__main__": - header_cli() \ No newline at end of file + header_cli() diff --git a/src/thd_json/header/header.json b/src/thd_json/header/header.json index 27312ff1b4b0c53ac4c21949833fd92ae2e2dd08..73ef475a91efe60c4d8eae1a1fa5a93127befdbf 100644 --- a/src/thd_json/header/header.json +++ b/src/thd_json/header/header.json @@ -1,9 +1,9 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "THD Projection File Schema", + "title": "THD Data Metadata Header File Schema", "type": "object", - "version": 0.2, - "date": "14.11.2024", + "version": 0.6, + "date": "16.11.2024", "properties": { "timestamp": { "type": "string", diff --git a/src/thd_json/image/__init__.py b/src/thd_json/image/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4dd5cb3706ea7d3f2c36811f61443543e135c302 --- /dev/null +++ b/src/thd_json/image/__init__.py @@ -0,0 +1,35 @@ +from thd_json import Validator + +from pathlib import Path +import argparse + + +def get_image_validator(json_suffix: str = "*.json") -> Validator: + return Validator(Path(__file__).parent / Path("image.json"), json_suffix) + + +def image_cli(): + parser = argparse.ArgumentParser( + description="JSON THD Image Validator CLI with uv." + ) + parser.add_argument("folder", help="Folder to check.", type=str) + parser.add_argument( + "suffix", help="Projection suffix.", default="*.json", type=Path, nargs="?" + ) + args = parser.parse_args() + + suffix = str(args.suffix) + if not suffix.startswith("*"): + raise ValueError(f'The suffix must always start with: "*". \nIt is: {suffix}') + + validator = get_image_validator(suffix) + + folder = Path(args.folder) + if not folder.exists(): + raise FileNotFoundError(f"Folder: {folder} does not exist") + + validator.folder(Path(args.folder)) + + +if __name__ == "__main__": + image_cli() diff --git a/src/thd_json/image/image.json b/src/thd_json/image/image.json new file mode 100644 index 0000000000000000000000000000000000000000..ff2f8822e69f5847f8fbdf15dc7bceb4519f9b4f --- /dev/null +++ b/src/thd_json/image/image.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "THD Projection Image File Schema", + "type": "object", + "version": 0.6, + "date": "19.11.2024", + "properties": { + "timestamp": { + "type": "string", + "description": "Timestamp, e.g. 2018-11-13T20:20:39+00:00", + "format": "date-time" + }, + "uuid": { + "type": "string", + "description": "Unique identifier", + "format": "uuid" + }, + "image_path": { + "type": "string", + "description": "Path to TIFF file.", + "pattern": "^.*\\.(tif|tiff)$" + } + }, + "required": [ + "timestamp", + "uuid", + "image_path" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/thd_json/projection/__init__.py b/src/thd_json/projection/__init__.py index 8cdd6116f4382c4979dcab781f983f5ac0984ea7..071e623aa5d872034e58c9dcbd205c4cebcdea7a 100644 --- a/src/thd_json/projection/__init__.py +++ b/src/thd_json/projection/__init__.py @@ -4,28 +4,32 @@ from pathlib import Path import argparse -def get_projection_validator(json_suffix: str = '*.json') -> Validator: - return Validator(Path(__file__).parent / Path('projection.json'), json_suffix) - -def projection_cli(): - parser = argparse.ArgumentParser(description='JSON THD Projection Validator CLI with uv.') - parser.add_argument('folder', help='Folder to check.', type=str) - parser.add_argument('suffix', help='Projection suffix.', default='*.json', type=Path, nargs='?') +def get_projection_validator(json_suffix: str = "*.json") -> Validator: + return Validator(Path(__file__).parent / Path("projection.json"), json_suffix) + + +def projection_geometry_cli(): + parser = argparse.ArgumentParser( + description="JSON THD Projection Geometry Validator CLI with uv." + ) + parser.add_argument("folder", help="Folder to check.", type=str) + parser.add_argument( + "suffix", help="Projection suffix.", default="*.json", type=Path, nargs="?" + ) args = parser.parse_args() suffix = str(args.suffix) - if not suffix.startswith('*'): + if not suffix.startswith("*"): raise ValueError(f'The suffix must always start with: "*". \nIt is: {suffix}') - validator = get_projection_validator( - suffix) - + validator = get_projection_validator(suffix) + folder = Path(args.folder) if not folder.exists(): - raise FileNotFoundError(f'Folder: {folder} does not exist') + raise FileNotFoundError(f"Folder: {folder} does not exist") validator.folder(Path(args.folder)) if __name__ == "__main__": - projection_cli() \ No newline at end of file + projection_geometry_cli() diff --git a/src/thd_json/projection/projection.json b/src/thd_json/projection/projection.json index fcafe946b991747f4715d05e49a68ec6853b62f3..7868d0bdc4257927d2928d8aed3898dfa6c7cf17 100644 --- a/src/thd_json/projection/projection.json +++ b/src/thd_json/projection/projection.json @@ -2,110 +2,30 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "THD Projection File Schema", "type": "object", - "version": 0.3, - "date": "14.11.2024", + "version": 0.6, + "date": "19.11.2024", "properties": { "header": { "$ref": "./header/header.json" }, - "focal_spot_position_mm": { - "type": "array", - "description": "Position of the focal spot in millimeters.", - "items": { - "type": "number" - }, - "minItems": 3, - "maxItems": 3 + "image": { + "$ref": "./image/image.json" }, - "focal_spot_orientation_quat": { - "type": "array", - "description": "Quaternion representing the detector's center orientation.", - "items": { - "type": "number" - }, - "minItems": 4, - "maxItems": 4 + "projection_geometry": { + "$ref": "./projection_geometry/projection_geometry.json" }, - "detector_center_position_mm": { - "type": "array", - "description": "Center position of the detector in millimeters.", - "items": { - "type": "number" - }, - "minItems": 3, - "maxItems": 3 + "detector": { + "$ref": "./detector/detector.json" }, - "detector_horizontal_vector": { - "type": "array", - "description": "Horizontal orientation vector of the detector.", - "items": { - "type": "number" - }, - "minItems": 3, - "maxItems": 3 - }, - "detector_vertical_vector": { - "type": "array", - "description": "Vertical orientation vector of the detector.", - "items": { - "type": "number" - }, - "minItems": 3, - "maxItems": 3 - }, - "detector_center_orientation_quat": { - "type": "array", - "description": "Quaternion representing the detector's center orientation.", - "items": { - "type": "number" - }, - "minItems": 4, - "maxItems": 4 - }, - "pixel_pitch_width_mm": { - "type": "number", - "description": "Pixel pitch in width dimension measured in millimeters.", - "minimum": 0.001 - }, - "pixel_pitch_height_mm": { - "type": "number", - "description": "Pixel pitch in height dimension measured in millimeters.", - "minimum": 0.001 - }, - "image_width_px": { - "type": "integer", - "description": "Width of the image in pixels.", - "minimum": 1 - }, - "image_height_px": { - "type": "integer", - "description": "Height of the image in pixels.", - "minimum": 1 - }, - "projection_matrix_cera": { - "type": "array", - "description": "Projection matrix for CERA, representing transformations.", - "items": { - "type": "array", - "items": { - "type": "number" - }, - "minItems": 4, - "maxItems": 4 - }, - "minItems": 3, - "maxItems": 3 + "source": { + "$ref": "./source/source.json" } }, "required": [ - "focal_spot_position_mm", - "detector_center_position_mm", - "detector_center_orientation_quat", - "pixel_pitch_height_mm", - "pixel_pitch_width_mm", - "image_width_px", - "image_height_px" + "header", + "projection_geometry", + "detector", + "image" ], "additionalProperties": false } - \ No newline at end of file diff --git a/src/thd_json/projection_geometry/__init__.py b/src/thd_json/projection_geometry/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c5316ee932ef2b0cf791856f646d4e86f25d8adc --- /dev/null +++ b/src/thd_json/projection_geometry/__init__.py @@ -0,0 +1,37 @@ +from thd_json import Validator + +from pathlib import Path +import argparse + + +def get_projection_geometry_validator(json_suffix: str = "*.json") -> Validator: + return Validator( + Path(__file__).parent / Path("projection_geometry.json"), json_suffix + ) + + +def projection_geometry_cli(): + parser = argparse.ArgumentParser( + description="JSON THD Projection Geometry Validator CLI with uv." + ) + parser.add_argument("folder", help="Folder to check.", type=str) + parser.add_argument( + "suffix", help="Projection suffix.", default="*.json", type=Path, nargs="?" + ) + args = parser.parse_args() + + suffix = str(args.suffix) + if not suffix.startswith("*"): + raise ValueError(f'The suffix must always start with: "*". \nIt is: {suffix}') + + validator = get_projection_geometry_validator(suffix) + + folder = Path(args.folder) + if not folder.exists(): + raise FileNotFoundError(f"Folder: {folder} does not exist") + + validator.folder(Path(args.folder)) + + +if __name__ == "__main__": + projection_geometry_cli() diff --git a/src/thd_json/projection_geometry/projection_geometry.json b/src/thd_json/projection_geometry/projection_geometry.json new file mode 100644 index 0000000000000000000000000000000000000000..bc376b524918ea96328a70cdaf7e630892b431b9 --- /dev/null +++ b/src/thd_json/projection_geometry/projection_geometry.json @@ -0,0 +1,70 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "THD Projection Geometry File Schema", + "type": "object", + "version": 0.6, + "date": "19.11.2024", + "properties": { + "focal_spot_position_mm": { + "type": "array", + "description": "Position of the focal spot in millimeters.", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "focal_spot_orientation_quat": { + "type": "array", + "description": "Quaternion representing the detector's center orientation.", + "items": { + "type": "number" + }, + "minItems": 4, + "maxItems": 4 + }, + "detector_center_position_mm": { + "type": "array", + "description": "Center position of the detector in millimeters.", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "detector_horizontal_vector": { + "type": "array", + "description": "Horizontal orientation vector of the detector.", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "detector_vertical_vector": { + "type": "array", + "description": "Vertical orientation vector of the detector.", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "detector_center_orientation_quat": { + "type": "array", + "description": "Quaternion representing the detector's center orientation.", + "items": { + "type": "number" + }, + "minItems": 4, + "maxItems": 4 + } + }, + "required": [ + "focal_spot_position_mm", + "detector_center_position_mm", + "detector_center_orientation_quat" + ], + "additionalProperties": false + } + \ No newline at end of file diff --git a/src/thd_json/projection_matrix/__init__.py b/src/thd_json/projection_matrix/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/thd_json/projection_matrix/projection_matrix.json b/src/thd_json/projection_matrix/projection_matrix.json new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/thd_json/source/__init__.py b/src/thd_json/source/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..62e56f51131dcba93fc66a69f0f8453df4ecfb58 --- /dev/null +++ b/src/thd_json/source/__init__.py @@ -0,0 +1,35 @@ +from thd_json import Validator + +from pathlib import Path +import argparse + + +def get_source_validator(json_suffix: str = "*.json") -> Validator: + return Validator(Path(__file__).parent / Path("source.json"), json_suffix) + + +def source_cli(): + parser = argparse.ArgumentParser( + description="JSON THD Source Validator CLI with uv." + ) + parser.add_argument("folder", help="Folder to check.", type=str) + parser.add_argument( + "suffix", help="Projection suffix.", default="*.json", type=Path, nargs="?" + ) + args = parser.parse_args() + + suffix = str(args.suffix) + if not suffix.startswith("*"): + raise ValueError(f'The suffix must always start with: "*". \nIt is: {suffix}') + + validator = get_source_validator(suffix) + + folder = Path(args.folder) + if not folder.exists(): + raise FileNotFoundError(f"Folder: {folder} does not exist") + + validator.folder(Path(args.folder)) + + +if __name__ == "__main__": + source_cli() diff --git a/src/thd_json/source/source.json b/src/thd_json/source/source.json new file mode 100644 index 0000000000000000000000000000000000000000..54feda7af7d77d262313ccff1e9b314cb9bf6a0e --- /dev/null +++ b/src/thd_json/source/source.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "THD X-Ray Source File Schema", + "type": "object", + "version": 0.6, + "date": "19.11.2024", + "properties": { + "voltage_kv": { + "type": "number", + "description": "Source voltage in kV.", + "minimum": 0.001 + }, + "current_ma": { + "type": "number", + "description": "Source current in µA.", + "minimum": 0.001 + } + }, + + "required": [ + "voltage_kv", + "current_ma" + ], + "additionalProperties": false + } + \ No newline at end of file diff --git a/src/thd_json/validation.py b/src/thd_json/validation.py index 754a534f6a5c0dd0bdbad11793540207f61644fc..40cf87505e900dbd893b464edf83a0648b16cfd7 100644 --- a/src/thd_json/validation.py +++ b/src/thd_json/validation.py @@ -1,37 +1,66 @@ -import os import json from pathlib import Path from jsonschema import ValidationError, RefResolver from jsonschema.validators import Draft202012Validator -import os -class Validator(): - def __init__(self, json_schema: Path, json_suffix: str = '*.json') -> None: +class Validator: + def __init__(self, json_schema: Path, json_suffix: str = "*.json") -> None: self.json_schema = json_schema self.json_suffix = json_suffix print(f"Validator for schema: {json_schema} \nSuffix: {json_suffix}\n") def file(self, file_path: Path) -> bool: - with open(str(self.json_schema)) as schema_file, open(str(file_path)) as data_file: + with open(str(self.json_schema)) as schema_file, open( + str(file_path) + ) as data_file: schema = json.load(schema_file) data = json.load(data_file) + schema_path = self.json_schema.parent.absolute().as_uri() + resolver = RefResolver(base_uri=schema_path, referrer=schema) + try: - schema_path = self.json_schema.parent.absolute().as_uri() - resolver = RefResolver(base_uri=schema_path, - referrer=schema) - Draft202012Validator(schema=schema, - format_checker=Draft202012Validator.FORMAT_CHECKER, - resolver=resolver).validate(data) + Draft202012Validator( + schema=schema, + format_checker=Draft202012Validator.FORMAT_CHECKER, + resolver=resolver, + ).validate(data) print(f"Validate file: {file_path.parent} / {file_path.name}\n") - + except ValidationError as e: print("Validation error:", e) return True - + def folder(self, folder_path: Path) -> bool: print(f"Validate folder: {folder_path}\n") for file in folder_path.glob(self.json_suffix): self.file(file) - return True \ No newline at end of file + return True + + def get_schema(self): + with open(str(self.json_schema)) as schema_file: + schema = json.load(schema_file) + schema_path = self.json_schema.parent.absolute().as_uri() + resolver = RefResolver(base_uri=schema_path, referrer=schema) + + return self.resolve_ref(resolver, schema) + + def resolve_ref(self, resolver, schema_part): + if isinstance(schema_part, dict) and "$ref" in schema_part: + # Resolve the reference + ref = schema_part["$ref"] + with resolver.resolving(ref) as resolved: + return self.resolve_ref( + resolver, resolved + ) # Recursively resolve nested references + elif isinstance(schema_part, dict): + # Resolve each value in the dictionary + return { + key: self.resolve_ref(resolver, value) + for key, value in schema_part.items() + } + elif isinstance(schema_part, list): + # Resolve each item in the list + return [self.resolve_ref(resolver, item) for item in schema_part] + return schema_part diff --git a/uv.lock b/uv.lock index 0ec452f68313febb762afffbd35fcf7dfbfb7f8c..a6e03ff7697f2e5f859e742ce704884d91cc594b 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "hypothesis" +version = "6.119.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/f2/9f8aa06c692549cbe2c4d8993bd8563092ca88fc65a6a58c52a400c89d67/hypothesis-6.119.0.tar.gz", hash = "sha256:ca441c6ef55d17f27f642fa08657e80f9c13d9da7ae191c8ad58fbc2f16acd1b", size = 411964 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/be/fd7deddef49491c01375345b1a21a5867603b021460c3efae5a16285197c/hypothesis-6.119.0-py3-none-any.whl", hash = "sha256:99d660bfdab62cfd7c9966e3829a217697671e0b992f0372b5b43647b3414471", size = 473116 }, +] + +[[package]] +name = "hypothesis-jsonschema" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hypothesis" }, + { name = "jsonschema" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/ad/2073dd29d8463a92c243d0c298370e50e0d4082bc67f156dc613634d0ec4/hypothesis-jsonschema-0.23.1.tar.gz", hash = "sha256:f4ac032024342a4149a10253984f5a5736b82b3fe2afb0888f3834a31153f215", size = 42896 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/44/635a8d2add845c9a2d99a93a379df77f7e70829f0a1d7d5a6998b61f9d01/hypothesis_jsonschema-0.23.1-py3-none-any.whl", hash = "sha256:a4d74d9516dd2784fbbae82e009f62486c9104ac6f4e3397091d98a1d5ee94a2", size = 29200 }, +] + [[package]] name = "json-schema" version = "0.3" @@ -128,17 +164,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/86/6f72984a284d720d84fba5ee7b0d1b0d320978b516497cbfd6e335e95a3e/rpds_py-0.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677", size = 219621 }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + [[package]] name = "thd-json" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "hypothesis" }, + { name = "hypothesis-jsonschema" }, { name = "json-schema" }, { name = "jsonschema" }, ] [package.metadata] requires-dist = [ + { name = "hypothesis", specifier = ">=6.119.0" }, + { name = "hypothesis-jsonschema", specifier = ">=0.23.1" }, { name = "json-schema", specifier = ">=0.3" }, { name = "jsonschema", specifier = ">=4.23.0" }, ]