textual-file-viewer 0.10.0a2__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.
@@ -0,0 +1,7 @@
1
+ .idea
2
+ __pycache__
3
+ build
4
+ dist
5
+ *.egg-info
6
+ snapshot_report.html
7
+ uv.lock
@@ -0,0 +1,147 @@
1
+ variables:
2
+ PYTHON_VERSION: "3.10"
3
+
4
+ stages:
5
+ - static_analysis
6
+ - test
7
+ - build
8
+ - publish
9
+
10
+ before_script:
11
+ - pip install --upgrade pip uv --root-user-action=ignore
12
+
13
+ .setup_python_venv: &setup_python_venv
14
+ - uv venv
15
+ - uv sync --group dev
16
+ - source .venv/bin/activate
17
+
18
+ pylint_mypy:
19
+ stage: static_analysis
20
+ image: python:${PYTHON_VERSION}
21
+ needs: []
22
+ interruptible: true
23
+ allow_failure: true
24
+ script:
25
+ - *setup_python_venv
26
+ - uv add pylint-gitlab
27
+ - pylint --output-format=pylint_gitlab.GitlabCodeClimateReporter --fail-under=5 --jobs=0 $(git ls-files '*.py') > codeclimate.json
28
+ - pylint --output-format=pylint_gitlab.GitlabPagesHtmlReporter --fail-under=5 --jobs=0 $(git ls-files '*.py') > pylint_results.html
29
+ - mypy src --namespace-packages --junit-xml mypy_junit.xml
30
+ artifacts:
31
+ expire_in: never
32
+ when: always
33
+ paths:
34
+ - pylint_results.html
35
+ - mypy_junit.xml
36
+ - codeclimate.json
37
+ reports:
38
+ junit: mypy_junit.xml
39
+ codequality: codeclimate.json
40
+
41
+ .parallel:
42
+ parallel:
43
+ matrix:
44
+ - PYTHON_VERSION: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
45
+
46
+ pytest:
47
+ # noinspection YAMLSchemaValidation
48
+ parallel: !reference [ .parallel,parallel ]
49
+ stage: test
50
+ needs: [ ] # Run when pipeline starts, don't wait for pylint to finish
51
+ image: python:${PYTHON_VERSION}
52
+ interruptible: true
53
+ script:
54
+ - *setup_python_venv
55
+ - echo -e "[run]\nomit = .venv/*" > .coveragerc
56
+ - pytest --junitxml=results.xml --cov=. --cov-report xml:coverage.xml --cov-report term tests/
57
+ coverage: '/TOTAL.*\s+(\d+%)$/'
58
+ artifacts:
59
+ expire_in: never
60
+ paths:
61
+ - results.xml
62
+ - coverage.xml
63
+ reports:
64
+ junit: results.xml
65
+ coverage_report:
66
+ coverage_format: cobertura
67
+ path: coverage.xml
68
+
69
+ build:
70
+ stage: build
71
+ image: python:${PYTHON_VERSION}
72
+ dependencies: [] # Don't load dependencies from previous stage
73
+ script:
74
+ # Set the version in pyproject.toml to the current git tag
75
+ - VERSION=$(git describe --exact-match --tags)
76
+ - uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version $VERSION
77
+ # Build the package
78
+ - uv build
79
+ artifacts:
80
+ paths:
81
+ - dist
82
+ only: # Only run this stage when a tag is created/pushed
83
+ - tags
84
+
85
+ # This stage publishes to our Gitlab package registry
86
+ gitlab_registry:
87
+ stage: publish
88
+ image: python:${PYTHON_VERSION}
89
+ dependencies: [build] # Load the artifacts from the build stage
90
+ variables:
91
+ # With the following variables, we tell uv how to publish our package.
92
+ # Gitlab has built in usernames and passwords for publish packages. This means you don't need to change them!
93
+ UV_PUBLISH_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi"
94
+ UV_PUBLISH_USERNAME: "gitlab-ci-token"
95
+ UV_PUBLISH_PASSWORD: "${CI_JOB_TOKEN}"
96
+ script:
97
+ - uv publish dist/*.whl
98
+ only: # Only run this stage when a tag is created/pushed
99
+ - tags
100
+
101
+ # This stage publishes to testpypi
102
+ testpypi_registry:
103
+ stage: publish
104
+ image: python:${PYTHON_VERSION}
105
+ dependencies: [build] # Load the artifacts from the build stage
106
+ id_tokens:
107
+ PYPI_ID_TOKEN:
108
+ aud: testpypi
109
+ script:
110
+ # https://docs.pypi.org/trusted-publishers/using-a-publisher/#gitlab-cicd
111
+ # https://docs.pypi.org/trusted-publishers/adding-a-publisher/#gitlab-cicd
112
+ - apt update && apt install -y jq
113
+ - uv venv
114
+ - uv pip install -U id==1.5.0
115
+
116
+ # Retrieve the OIDC token from GitLab CI/CD, and exchange it for a PyPI API token
117
+ - oidc_token=$(uv run -m id PYPI)
118
+ - resp=$(curl -X POST https://test.pypi.org/_/oidc/mint-token -d "{\"token\":\"${oidc_token}\"}")
119
+ - api_token=$(jq --raw-output '.token' <<< "${resp}")
120
+
121
+ # Upload to PyPI authenticating via the newly-minted token
122
+ - uv publish -u __token__ -p "${api_token}" dist/* --publish-url "https://test.pypi.org/legacy/"
123
+ only: # Only run this stage when a tag is created/pushed
124
+ - tags
125
+
126
+ # This stage publishes to pypi
127
+ pypi_registry:
128
+ stage: publish
129
+ image: python:${PYTHON_VERSION}
130
+ dependencies: [build] # Load the artifacts from the build stage
131
+ id_tokens:
132
+ PYPI_ID_TOKEN:
133
+ aud: pypi
134
+ script:
135
+ - apt update && apt install -y jq
136
+ - uv venv
137
+ - uv pip install -U id==1.5.0
138
+
139
+ # Retrieve the OIDC token from GitLab CI/CD, and exchange it for a PyPI API token
140
+ - oidc_token=$(uv run -m id PYPI)
141
+ - resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\":\"${oidc_token}\"}")
142
+ - api_token=$(jq --raw-output '.token' <<< "${resp}")
143
+
144
+ # Upload to PyPI authenticating via the newly-minted token
145
+ - uv publish -u __token__ -p "${api_token}" dist/*
146
+ only: # Only run this stage when a tag is created/pushed
147
+ - tags
@@ -0,0 +1,2 @@
1
+ include src/textual_file_viewer/*.tcss
2
+ include src/textual_file_viewer/*.ttf
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.5
2
+ Name: textual-file-viewer
3
+ Version: 0.10.0a2
4
+ Summary: View the contents of several common file types
5
+ Project-URL: Repository, https://gitlab.com/pjhdekoning/textual-file-viewer
6
+ Project-URL: Issues, https://gitlab.com/pjhdekoning/textual-file-viewer/issues
7
+ Author-email: Patrick de Koning <pjhdekoning@lumc.nl>
8
+ Keywords: TUI
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Healthcare Industry
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: numpy
22
+ Requires-Dist: openpyxl
23
+ Requires-Dist: pandas>=2.0
24
+ Requires-Dist: pillow
25
+ Requires-Dist: pydicom
26
+ Requires-Dist: pylibjpeg[libjpeg,openjpeg]
27
+ Requires-Dist: rich-pixels>=3.0.1
28
+ Requires-Dist: simpleitk>=2.4.1
29
+ Requires-Dist: textual-image>=0.7.0
30
+ Requires-Dist: textual-slider>=0.2.0
31
+ Requires-Dist: textual-sortable-datatable
32
+ Requires-Dist: textual>=1.0
33
+ Requires-Dist: typer
34
+ Description-Content-Type: text/markdown
35
+
36
+ # textual-file-viewer
37
+ ![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https://gitlab.com/pjhdekoning/textual-file-viewer/-/raw/main/pyproject.toml?ref_type=heads)
38
+ ![Gitlab Pipeline Status](https://img.shields.io/gitlab/pipeline-status/pjhdekoning/textual-file-viewer)
39
+ ![GitLab Last Commit](https://img.shields.io/gitlab/last-commit/pjhdekoning/textual-file-viewer)
40
+ ![GitLab Issues](https://img.shields.io/gitlab/issues/open/pjhdekoning/textual-file-viewer)
41
+ ![PyPI - License](https://img.shields.io/pypi/l/textual-file-viewer)
42
+ ![PyPI - Status](https://img.shields.io/pypi/status/textual-file-viewer)
43
+ ![PyPI - Version](https://img.shields.io/pypi/v/textual-file-viewer)
44
+ ![PyPI - Format](https://img.shields.io/pypi/format/textual-file-viewer)
45
+ ![Pepy Total Downloads](https://img.shields.io/pepy/dt/textual-file-viewer)
46
+
47
+ Commandline tool to view .xslx, .csv, DICOM, MetaImage (.mha/.mhd) and Niftii files.
48
+ ```shell
49
+ file-viewer FILENAME
50
+ ```
@@ -0,0 +1,15 @@
1
+ # textual-file-viewer
2
+ ![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https://gitlab.com/pjhdekoning/textual-file-viewer/-/raw/main/pyproject.toml?ref_type=heads)
3
+ ![Gitlab Pipeline Status](https://img.shields.io/gitlab/pipeline-status/pjhdekoning/textual-file-viewer)
4
+ ![GitLab Last Commit](https://img.shields.io/gitlab/last-commit/pjhdekoning/textual-file-viewer)
5
+ ![GitLab Issues](https://img.shields.io/gitlab/issues/open/pjhdekoning/textual-file-viewer)
6
+ ![PyPI - License](https://img.shields.io/pypi/l/textual-file-viewer)
7
+ ![PyPI - Status](https://img.shields.io/pypi/status/textual-file-viewer)
8
+ ![PyPI - Version](https://img.shields.io/pypi/v/textual-file-viewer)
9
+ ![PyPI - Format](https://img.shields.io/pypi/format/textual-file-viewer)
10
+ ![Pepy Total Downloads](https://img.shields.io/pepy/dt/textual-file-viewer)
11
+
12
+ Commandline tool to view .xslx, .csv, DICOM, MetaImage (.mha/.mhd) and Niftii files.
13
+ ```shell
14
+ file-viewer FILENAME
15
+ ```
@@ -0,0 +1,8 @@
1
+ import sys
2
+ sys.path.append("src")
3
+
4
+ from textual_file_viewer.file_viewer import app # pylint: disable=wrong-import-position
5
+
6
+
7
+ if __name__ == "__main__":
8
+ app()
@@ -0,0 +1,108 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "textual-file-viewer"
7
+ description = "View the contents of several common file types"
8
+ keywords = ["TUI"]
9
+ version = "0.10.0-a2"
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ classifiers = [
13
+ "Operating System :: OS Independent",
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Intended Audience :: Healthcare Industry",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Natural Language :: English",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Environment :: Console",
22
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
23
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
24
+ ]
25
+
26
+ dependencies = [
27
+ "numpy",
28
+ "openpyxl",
29
+ "pandas>=2.0",
30
+ "pillow",
31
+ "pydicom",
32
+ "pylibjpeg[libjpeg,openjpeg]",
33
+ "rich-pixels>=3.0.1",
34
+ "simpleitk>=2.4.1",
35
+ "textual-image>=0.7.0",
36
+ "textual-slider>=0.2.0",
37
+ "textual-sortable-datatable",
38
+ "textual>=1.0",
39
+ "typer",
40
+ ]
41
+
42
+ authors = [
43
+ { name = "Patrick de Koning", email = "pjhdekoning@lumc.nl" },
44
+ ]
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "mypy>=1.14.1",
49
+ "pylint-gitlab",
50
+ "pylint>=3.3.7",
51
+ "pytest-cov",
52
+ "pytest-textual-snapshot>=1.1.0",
53
+ "ruff>=0.13.0",
54
+ "textual-dev>=1.7.0",
55
+ "ty>=0.0.1a20",
56
+ "types-python-dateutil>=2.9.0.20241206",
57
+ ]
58
+
59
+ [project.urls]
60
+ Repository = "https://gitlab.com/pjhdekoning/textual-file-viewer"
61
+ Issues = "https://gitlab.com/pjhdekoning/textual-file-viewer/issues"
62
+
63
+ [project.scripts]
64
+ file-viewer = "textual_file_viewer.file_viewer:app"
65
+ dicom-viewer = "textual_file_viewer.dicom_viewer:app"
66
+ mhd-viewer = "textual_file_viewer.mhd_nifti_viewer:app"
67
+
68
+ # PYLINT
69
+ [tool.pylint.main]
70
+ init-hook = 'import sys; sys.path.append("."); sys.path.append("src")'
71
+
72
+ [tool.pylint."messages control"]
73
+ disable = [
74
+ "missing-function-docstring",
75
+ "missing-module-docstring",
76
+ "missing-class-docstring",
77
+ "logging-format-interpolation",
78
+ "logging-fstring-interpolation",
79
+ "no-name-in-module",
80
+ "duplicate-code"
81
+ ]
82
+
83
+ [tool.pylint.format]
84
+ max-line-length = 125
85
+ max-module-lines = 1000
86
+
87
+ [tool.pylint.design]
88
+ min-public-methods = 1
89
+ max-args = 10
90
+ max-locals = 25
91
+
92
+ [tool.pylint.basic]
93
+ good-names = ["e", "f", "i", "j", "k", "m", "x", "y", "z", "_", "id", "df", "md"]
94
+
95
+ [tool.pylint.exceptions]
96
+ overgeneral-exceptions = ["builtins.Exception"]
97
+
98
+ # MYPY
99
+ [tool.mypy]
100
+ warn_redundant_casts = true
101
+ ignore_missing_imports = true
102
+ disallow_untyped_calls = true
103
+ disallow_untyped_defs = true
104
+ check_untyped_defs = true
105
+ warn_return_any = true
106
+ no_implicit_optional = true
107
+ strict_optional = true
108
+ mypy_path = "src"
@@ -0,0 +1,129 @@
1
+ import pydicom
2
+ from pydicom import DataElement, Dataset
3
+ from rich.markup import escape
4
+
5
+
6
+ def dsrdump(ds: pydicom.Dataset, depth: int = 0, result: list[str] | None = None) -> list[str]:
7
+ """
8
+ Recursively dumps a DICOM SR dataset mimicking DCMTK dsrdump output format,
9
+ enhanced with rich colored terminal output.
10
+ """
11
+ # Top-level SR Document Root
12
+ if result is None:
13
+ result = [top_level_document_root(ds)]
14
+
15
+ # Traverse Content Sequence items if present
16
+ content_seq = ds.get("ContentSequence", [])
17
+ for item in content_seq:
18
+ value_type = escape(item.get("ValueType", "CONTAINER"))
19
+ rel_type = escape(item.get("RelationshipType", ""))
20
+
21
+ # Build Code Meaning string (Concept Name)
22
+ concept_str = code_meaning(item)
23
+
24
+ # Build Value / Content representation based on ValueType
25
+ val_str = value_content_representation(item, value_type)
26
+
27
+ indent = " " * depth
28
+ # Construct and print the formatted line
29
+ line_parts = [p for p in [
30
+ f"[cyan]<{rel_type}>[/cyan]",
31
+ f"[magenta]{value_type}:[/magenta]",
32
+ f"[green]{concept_str}[/green]",
33
+ f"[yellow]{val_str}[/yellow]"
34
+ ] if p]
35
+ result.append(indent + " ".join(line_parts))
36
+
37
+ # Recurse for nested child sequences
38
+ if "ContentSequence" in item:
39
+ dsrdump(item, depth=depth + 1, result=result)
40
+ return result
41
+
42
+
43
+ def top_level_document_root(ds: Dataset) -> str:
44
+ if "ConceptNameCodeSequence" in ds and len(ds.ConceptNameCodeSequence) > 0:
45
+ item = ds.ConceptNameCodeSequence[0]
46
+ val = escape(item.get("CodeValue", ""))
47
+ scheme = escape(item.get("CodingSchemeDesignator", ""))
48
+ meaning = escape(item.get("CodeMeaning", ""))
49
+ doc_title = f'({val},{scheme},"{meaning}")'
50
+ return f"[green]{doc_title}[/green] "
51
+
52
+ return ''
53
+
54
+
55
+ def code_meaning(item: Dataset) -> str:
56
+ concept_str = ""
57
+ if "ConceptNameCodeSequence" in item and len(item.ConceptNameCodeSequence) > 0:
58
+ c = item.ConceptNameCodeSequence[0]
59
+ c_val = escape(c.get("CodeValue", ""))
60
+ c_scheme = escape(c.get("CodingSchemeDesignator", ""))
61
+ c_meaning = escape(c.get("CodeMeaning", ""))
62
+ concept_str = f'({c_val},{c_scheme},"{c_meaning}")'
63
+ return concept_str
64
+
65
+
66
+ def value_content_representation(item: Dataset, value_type: str) -> str:
67
+ val_str = ""
68
+ if value_type == "TEXT":
69
+ val_str = f'= "{escape(item.get("TextValue", ""))}"'
70
+ elif value_type == "CODE":
71
+ val_str = concept_value_representation(item, val_str)
72
+ elif value_type in ("NUM", "NUMERIC"):
73
+ val_str = numeric_value_representation(item, val_str)
74
+ elif value_type == "PNAME":
75
+ val_str = f'= "{escape(str(item.get("PersonName", "")))}"'
76
+ elif value_type == "UIDREF":
77
+ val_str = f"= {escape(str(item.get('UID', '')))}"
78
+ elif value_type == "DATE":
79
+ val_str = f"= {escape(str(item.get('Date', '')))}"
80
+ elif value_type == "TIME":
81
+ val_str = f"= {escape(str(item.get('Time', '')))}"
82
+ elif value_type == "DATETIME":
83
+ val_str = f"= {escape(str(item.get('DateTime', '')))}"
84
+ elif value_type == "IMAGE":
85
+ val_str = image_content_representation(item, val_str)
86
+ elif value_type == "CONTAINER":
87
+ continuity = escape(item.get("ContinuityOfContent", ""))
88
+ if continuity:
89
+ val_str = f"\\[{continuity}]"
90
+ return val_str
91
+
92
+
93
+ def image_content_representation(item: Dataset, val_str: str) -> str:
94
+ if "ReferencedSOPSequence" in item and len(item.ReferencedSOPSequence) > 0:
95
+ ref = item.ReferencedSOPSequence[0]
96
+ ref_class = escape(str(ref.get("ReferencedSOPClassUID", "")))
97
+ ref_inst = escape(str(ref.get("ReferencedSOPInstanceUID", "")))
98
+ val_str = f"= ({ref_class},{ref_inst})"
99
+ return val_str
100
+
101
+
102
+ def concept_value_representation(item: Dataset, val_str: str) -> str:
103
+ if "ConceptCodeSequence" in item and len(item.ConceptCodeSequence) > 0:
104
+ cc = item.ConceptCodeSequence[0]
105
+ cc_val = escape(cc.get("CodeValue", ""))
106
+ cc_scheme = escape(cc.get("CodingSchemeDesignator", ""))
107
+ cc_meaning = escape(cc.get("CodeMeaning", ""))
108
+ val_str = f'= ({cc_val},{cc_scheme},"{cc_meaning}")'
109
+ return val_str
110
+
111
+
112
+ def numeric_value_representation(item: Dataset, val_str: str) -> str:
113
+ num_val = ""
114
+ if "MeasuredValueSequence" in item and len(item.MeasuredValueSequence) > 0:
115
+ mv = item.MeasuredValueSequence[0]
116
+ num = escape(str(mv.get("NumericValue", "")))
117
+ unit_str = ""
118
+ if (
119
+ "MeasurementUnitsCodeSequence" in mv
120
+ and len(mv.MeasurementUnitsCodeSequence) > 0
121
+ ):
122
+ u = mv.MeasurementUnitsCodeSequence[0]
123
+ u_val = escape(u.get("CodeValue", ""))
124
+ u_scheme = escape(u.get("CodingSchemeDesignator", ""))
125
+ u_meaning = escape(u.get("CodeMeaning", ""))
126
+ unit_str = f'({u_val},{u_scheme},"{u_meaning}")'
127
+ num_val = f"= {num} {unit_str}".strip()
128
+ val_str = num_val
129
+ return val_str