dcmspec 0.2.1__py3-none-any.whl
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.
- dcmspec/__init__.py +0 -0
- dcmspec/apps/__init__.py +1 -0
- dcmspec/apps/cli/__init__.py +0 -0
- dcmspec/apps/cli/dataelements.py +89 -0
- dcmspec/apps/cli/iodattributes.py +125 -0
- dcmspec/apps/cli/iodmodules.py +92 -0
- dcmspec/apps/cli/modattributes.py +265 -0
- dcmspec/apps/cli/tdwiicontent.py +365 -0
- dcmspec/apps/cli/uidvalues.py +84 -0
- dcmspec/apps/cli/upsdimseattributes.py +109 -0
- dcmspec/apps/cli/upsioddimseattributes.py +330 -0
- dcmspec/apps/ui/iod_explorer/README.md +33 -0
- dcmspec/apps/ui/iod_explorer/__init__.py +9 -0
- dcmspec/apps/ui/iod_explorer/config/README.md +171 -0
- dcmspec/apps/ui/iod_explorer/config/iod_explorer_config.json +4 -0
- dcmspec/apps/ui/iod_explorer/config/iod_explorer_config_debug.json +4 -0
- dcmspec/apps/ui/iod_explorer/config/iod_explorer_config_example.json +4 -0
- dcmspec/apps/ui/iod_explorer/config/iod_explorer_config_minimal_logging.json +4 -0
- dcmspec/apps/ui/iod_explorer/iod_explorer.py +989 -0
- dcmspec/config.py +90 -0
- dcmspec/csv_table_spec_parser.py +85 -0
- dcmspec/doc_handler.py +214 -0
- dcmspec/dom_table_spec_parser.py +831 -0
- dcmspec/dom_utils.py +116 -0
- dcmspec/iod_spec_builder.py +444 -0
- dcmspec/iod_spec_printer.py +59 -0
- dcmspec/json_spec_store.py +110 -0
- dcmspec/module_registry.py +51 -0
- dcmspec/pdf_doc_handler.py +451 -0
- dcmspec/progress.py +232 -0
- dcmspec/service_attribute_defaults.py +124 -0
- dcmspec/service_attribute_model.py +231 -0
- dcmspec/spec_factory.py +451 -0
- dcmspec/spec_merger.py +536 -0
- dcmspec/spec_model.py +461 -0
- dcmspec/spec_parser.py +37 -0
- dcmspec/spec_printer.py +131 -0
- dcmspec/spec_store.py +50 -0
- dcmspec/ups_xhtml_doc_handler.py +117 -0
- dcmspec/xhtml_doc_handler.py +182 -0
- dcmspec-0.2.1.dist-info/METADATA +139 -0
- dcmspec-0.2.1.dist-info/RECORD +45 -0
- dcmspec-0.2.1.dist-info/WHEEL +4 -0
- dcmspec-0.2.1.dist-info/entry_points.txt +11 -0
- dcmspec-0.2.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""CLI for extracting, merging, caching, and printing DICOM UPS IOD attributes from Part 3 and Part 4.
|
|
2
|
+
|
|
3
|
+
Features:
|
|
4
|
+
- Download and parse DICOM UPS IOD (Unified Procedure Step Information Object Definition) from Part 3.
|
|
5
|
+
- Merge with UPS DIMSE service requirements and role from Part 4.
|
|
6
|
+
- Cache the merged model as a JSON file for future runs and as a structured representation of the standard.
|
|
7
|
+
- Print the resulting merged attributes as a table or tree.
|
|
8
|
+
- Supports caching, configuration files, and command-line options for flexible workflows.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
poetry run python -m src.dcmspec.apps.cli.upsioddimseattributes [options]
|
|
12
|
+
|
|
13
|
+
For more details, use the --help option.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
from anytree import PreOrderIter
|
|
22
|
+
from dcmspec.config import Config
|
|
23
|
+
from dcmspec.dom_table_spec_parser import DOMTableSpecParser
|
|
24
|
+
from dcmspec.iod_spec_builder import IODSpecBuilder
|
|
25
|
+
from dcmspec.spec_factory import SpecFactory
|
|
26
|
+
from dcmspec.spec_merger import SpecMerger
|
|
27
|
+
from dcmspec.service_attribute_model import ServiceAttributeModel
|
|
28
|
+
from dcmspec.ups_xhtml_doc_handler import UPSXHTMLDocHandler
|
|
29
|
+
from dcmspec.service_attribute_defaults import UPS_DIMSE_MAPPING, UPS_COLUMNS_MAPPING, UPS_NAME_ATTR
|
|
30
|
+
from dcmspec.spec_printer import SpecPrinter
|
|
31
|
+
from dcmspec.json_spec_store import JSONSpecStore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def dicom_service_default_type(node, merged_model, service_model, default_attr, default_value):
|
|
35
|
+
"""Determine the default type value for a node based on its parent context in the service model.
|
|
36
|
+
|
|
37
|
+
See PS3.3 Section 5.5 Types and Conditions in Normalized IODs
|
|
38
|
+
|
|
39
|
+
When a Normalized IOD in PS3.3 invokes Modules (e.g., the SOP Common Module) or Attribute Macros that are specified
|
|
40
|
+
with Data Element Types, those specified Data Element Types and Conditions do not apply.
|
|
41
|
+
Rather, the Data Element Types and Conditions have to be specified for each Attribute for both SCU and SCP in the
|
|
42
|
+
appropriate Service definition in PS3.4.
|
|
43
|
+
|
|
44
|
+
- If the node is a direct child of a module node, and the module has a "catch-all" row in the service model
|
|
45
|
+
(e.g., "All other Attributes of ..."), use the value of default_attr from that row as the default.
|
|
46
|
+
- If no such parent context is found, use the provided default_value.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
node: The node for which to determine the default type.
|
|
50
|
+
merged_model: The merged model (not used here, but provided for interface compatibility).
|
|
51
|
+
service_model: The DICOM service attribute model to search for catch-all rows.
|
|
52
|
+
default_attr: The attribute to use as the default (e.g., "elem_type").
|
|
53
|
+
default_value: The fallback value if no catch-all row is found.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The default value for the type attribute, either from a catch-all row or the provided default.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
# Only apply the catch-all if the node is a direct child of a module node (i.e., grandparent is "content")
|
|
60
|
+
parent = node.parent
|
|
61
|
+
if parent is not None and parent.parent is not None and parent.parent.name == "content":
|
|
62
|
+
# Use the module attribute of the direct parent module node, fallback to name
|
|
63
|
+
module_name = getattr(parent, "module", parent.name)
|
|
64
|
+
# Search for a "catch-all" row in the service model for this module
|
|
65
|
+
pattern = re.compile(
|
|
66
|
+
rf"All (other )?Attributes of( the)? {re.escape(module_name)}( Module)?$"
|
|
67
|
+
)
|
|
68
|
+
for node in service_model.content.descendants:
|
|
69
|
+
node_name = getattr(node, "elem_name", None)
|
|
70
|
+
if node_name and pattern.match(node_name):
|
|
71
|
+
# Found a catch-all row: use its value for default_attr
|
|
72
|
+
val = getattr(node, default_attr, default_value)
|
|
73
|
+
logging.getLogger("modattributes").debug(
|
|
74
|
+
f"Set default {default_attr} for node '{getattr(node, 'name', None)}' "
|
|
75
|
+
f"(direct child of module '{module_name}') to '{val}'"
|
|
76
|
+
)
|
|
77
|
+
return val
|
|
78
|
+
|
|
79
|
+
# No catch-all row found or not a direct child of a module: use the provided default_value
|
|
80
|
+
logging.getLogger("modattributes").debug(
|
|
81
|
+
f"Set default {default_attr} for node '{getattr(node, 'name', None)}' "
|
|
82
|
+
f"(no direct module parent match) to '{default_value}'"
|
|
83
|
+
)
|
|
84
|
+
return default_value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def align_type_with_dimse_req(model, dimse_req_attributes, dimse_attributes):
|
|
88
|
+
"""Aligns the "Type" (elem_type) attribute in a DICOM model with the selected DIMSE service/role requirements.
|
|
89
|
+
|
|
90
|
+
This function ensures that the correct type attribute is present for each node in the model,
|
|
91
|
+
according to the selected DIMSE service (e.g., N-CREATE, N-SET) and role (e.g., SCU, SCP).
|
|
92
|
+
It removes or moves the "elem_type" attribute as appropriate, so that only the relevant
|
|
93
|
+
DIMSE-specific attribute (e.g., "dimse_ncreate", "dimse_nset") is present.
|
|
94
|
+
|
|
95
|
+
The function performs the following steps:
|
|
96
|
+
1. Removes the "Type" column from the model's metadata if present.
|
|
97
|
+
2. For each node in the model:
|
|
98
|
+
- If the node is not a DICOM attribute (i.e., not an element with both elem_name and elem_tag),
|
|
99
|
+
remove the "elem_type" attribute.
|
|
100
|
+
- If the node already has the required DIMSE attribute (e.g., "dimse_ncreate"), remove "elem_type"
|
|
101
|
+
(DIMSE takes precedence).
|
|
102
|
+
- If the node has "elem_type" but not the required DIMSE attribute, move the value from "elem_type"
|
|
103
|
+
to the DIMSE attribute (or to "dimse_all" if no specific DIMSE attribute is required).
|
|
104
|
+
3. Logs debug information for the first 20 processed nodes.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
model: The SpecModel to align.
|
|
108
|
+
dimse_req_attributes: List of required DIMSE attribute names for the selected service/role
|
|
109
|
+
(e.g., ["dimse_ncreate"]).
|
|
110
|
+
dimse_attributes: List of all DIMSE attribute names for the selected service/role.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
None. The model is modified in place.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
if not dimse_req_attributes:
|
|
117
|
+
dimse_req_attr = dimse_attributes[0] # Handle ALL_DIMSE case
|
|
118
|
+
else:
|
|
119
|
+
dimse_req_attr = dimse_req_attributes[0] # Handle C-FIND case
|
|
120
|
+
|
|
121
|
+
# Remove "Type" column from metadata if present
|
|
122
|
+
if hasattr(model.metadata, "header") and "Type" in model.metadata.header:
|
|
123
|
+
# Remove from header
|
|
124
|
+
idx = model.metadata.header.index("Type")
|
|
125
|
+
model.metadata.header.pop(idx)
|
|
126
|
+
# Remove from column_to_attr
|
|
127
|
+
if hasattr(model.metadata, "column_to_attr"):
|
|
128
|
+
# Find the key for "elem_type"
|
|
129
|
+
keys_to_remove = [k for k, v in model.metadata.column_to_attr.items() if v == "elem_type"]
|
|
130
|
+
for k in keys_to_remove:
|
|
131
|
+
model.metadata.column_to_attr.pop(k)
|
|
132
|
+
|
|
133
|
+
for node in PreOrderIter(model.content):
|
|
134
|
+
# Remove elem_type from all non DICOM Attribute nodes (e.g., module nodes)
|
|
135
|
+
if hasattr(node, "elem_type") and not (hasattr(node, "elem_name") and hasattr(node, "elem_tag")):
|
|
136
|
+
delattr(node, "elem_type")
|
|
137
|
+
# If the node already has the DIMSE-required attribute, remove elem_type (DIMSE takes precedence)
|
|
138
|
+
elif hasattr(node, dimse_req_attr):
|
|
139
|
+
if hasattr(node, "elem_type"):
|
|
140
|
+
delattr(node, "elem_type")
|
|
141
|
+
# If the node has elem_type but not the DIMSE-required attribute, move elem_type to the DIMSE attribute
|
|
142
|
+
elif hasattr(node, "elem_type"):
|
|
143
|
+
if dimse_req_attributes:
|
|
144
|
+
setattr(node, dimse_req_attr, getattr(node, "elem_type"))
|
|
145
|
+
else:
|
|
146
|
+
# If no specific DIMSE attribute, set to 'dimse_all'
|
|
147
|
+
setattr(node, "dimse_all", getattr(node, "elem_type"))
|
|
148
|
+
delattr(node, "elem_type")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main():
|
|
152
|
+
"""CLI for parsing, merging, caching, and printing DICOM UPS IOD attributes aligned with DIMSE service requirements.
|
|
153
|
+
|
|
154
|
+
This CLI downloads, merges, caches, and prints the attributes for a DICOM UPS (Unified Procedure Step) IOD
|
|
155
|
+
(Information Object Definition) from Part 3, aligned with the requirements of a selected DIMSE service and role
|
|
156
|
+
from Part 4.
|
|
157
|
+
|
|
158
|
+
The tool parses the IOD table and all referenced module attribute tables, then merges in the UPS DIMSE service
|
|
159
|
+
requirements (e.g., N-CREATE, N-SET, N-GET, C-FIND, FINAL) and role (SCU or SCP) from Part 4. The output can be
|
|
160
|
+
printed as a table or tree.
|
|
161
|
+
|
|
162
|
+
The resulting model is cached as a JSON file. The primary purpose of this cache file is to provide a structured,
|
|
163
|
+
machine-readable representation of the merged IOD and DIMSE service attributes, which can be used for further
|
|
164
|
+
processing or integration in other tools. As a secondary benefit, the cache file is also used to speed up
|
|
165
|
+
subsequent runs of the CLI scripts.
|
|
166
|
+
|
|
167
|
+
Usage:
|
|
168
|
+
poetry run python -m src.dcmspec.apps.cli.upsioddimseattributes [options]
|
|
169
|
+
|
|
170
|
+
Options:
|
|
171
|
+
--config (str): Path to the configuration file.
|
|
172
|
+
--dimse (str): DIMSE service to use (e.g., ALL_DIMSE, N-CREATE, N-SET, N-GET, C-FIND, FINAL).
|
|
173
|
+
--role (str): DIMSE role to use (SCU or SCP).
|
|
174
|
+
--print-mode (str): Print as 'table' (default), 'tree', or 'none' to skip printing.
|
|
175
|
+
-v, --verbose: Enable verbose (info-level) logging to the console.
|
|
176
|
+
-d, --debug: Enable debug logging to the console (overrides --verbose).
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
poetry run python -m src.dcmspec.apps.cli.upsioddimseattributes --dimse N-CREATE --role SCU --print-mode tree
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
parser = argparse.ArgumentParser()
|
|
183
|
+
parser.add_argument("--config", help="Path to the configuration file")
|
|
184
|
+
parser.add_argument("--dimse", default="ALL_DIMSE", help="DIMSE service to use (e.g. N-CREATE, N-SET, N-GET, etc.)")
|
|
185
|
+
parser.add_argument("--role", help="DIMSE role to use (e.g. SCU, SCP)")
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--print-mode",
|
|
188
|
+
choices=["table", "tree", "none"],
|
|
189
|
+
default="table",
|
|
190
|
+
help="Print as 'table' (default), 'tree', or 'none' to skip printing"
|
|
191
|
+
)
|
|
192
|
+
parser.add_argument(
|
|
193
|
+
"-d", "--debug",
|
|
194
|
+
action="store_true",
|
|
195
|
+
help="Enable debug logging to the console (overrides --verbose)"
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
"-v", "--verbose",
|
|
199
|
+
action="store_true",
|
|
200
|
+
help="Enable verbose (info-level) logging to the console"
|
|
201
|
+
)
|
|
202
|
+
args = parser.parse_args()
|
|
203
|
+
|
|
204
|
+
# Set up logger
|
|
205
|
+
logger = logging.getLogger("modattributes")
|
|
206
|
+
handler = logging.StreamHandler()
|
|
207
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
208
|
+
handler.setFormatter(formatter)
|
|
209
|
+
if not logger.hasHandlers():
|
|
210
|
+
logger.addHandler(handler)
|
|
211
|
+
if args.debug:
|
|
212
|
+
logger.setLevel(logging.DEBUG)
|
|
213
|
+
handler.setLevel(logging.DEBUG)
|
|
214
|
+
elif args.verbose:
|
|
215
|
+
logger.setLevel(logging.INFO)
|
|
216
|
+
handler.setLevel(logging.INFO)
|
|
217
|
+
else:
|
|
218
|
+
logger.setLevel(logging.WARNING)
|
|
219
|
+
handler.setLevel(logging.WARNING)
|
|
220
|
+
|
|
221
|
+
# Determine config file location
|
|
222
|
+
config_file = args.config or os.getenv("DCMSPEC_CONFIG", None)
|
|
223
|
+
config = Config(app_name="upsioddimse", config_file=config_file)
|
|
224
|
+
|
|
225
|
+
logger.debug(f"Config file: {config_file}")
|
|
226
|
+
logger.debug(f"Cache dir: {config.get_param('cache_dir')}")
|
|
227
|
+
|
|
228
|
+
# --- Build the IOD Spec Model (model 1) ---
|
|
229
|
+
iod_url = "https://dicom.nema.org/medical/dicom/current/output/html/part03.html"
|
|
230
|
+
iod_cache_file = "Part3.xhtml"
|
|
231
|
+
iod_table_id = "table_B.26.2-1"
|
|
232
|
+
iod_model_file = "Part3_table_B.26.2-1_expanded.json"
|
|
233
|
+
|
|
234
|
+
iod_factory = SpecFactory(
|
|
235
|
+
column_to_attr={0: "module", 1: "ref", 2: "usage"},
|
|
236
|
+
name_attr="module",
|
|
237
|
+
config=config,
|
|
238
|
+
logger=logger
|
|
239
|
+
)
|
|
240
|
+
module_factory = SpecFactory(
|
|
241
|
+
column_to_attr={0: "elem_name", 1: "elem_tag", 2: "elem_type", 3: "elem_description"},
|
|
242
|
+
name_attr="elem_name",
|
|
243
|
+
parser_kwargs={"skip_columns": [2]},
|
|
244
|
+
config=config,
|
|
245
|
+
logger=logger
|
|
246
|
+
)
|
|
247
|
+
builder = IODSpecBuilder(iod_factory=iod_factory, module_factory=module_factory, logger=logger)
|
|
248
|
+
iod_model, _ = builder.build_from_url(
|
|
249
|
+
url=iod_url,
|
|
250
|
+
cache_file_name=iod_cache_file,
|
|
251
|
+
json_file_name=iod_model_file,
|
|
252
|
+
table_id=iod_table_id,
|
|
253
|
+
force_download=False,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# --- Build the UPS Attribute Spec Model (model 2) ---
|
|
257
|
+
ups_url = "https://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_CC.2.5.html"
|
|
258
|
+
ups_cache_file = "UPSattributes.xhtml"
|
|
259
|
+
json_file_name = "UPSattributes.json"
|
|
260
|
+
ups_table_id = "table_CC.2.5-3"
|
|
261
|
+
|
|
262
|
+
ups_factory = SpecFactory(
|
|
263
|
+
model_class=ServiceAttributeModel,
|
|
264
|
+
input_handler=UPSXHTMLDocHandler(config=config),
|
|
265
|
+
table_parser=DOMTableSpecParser(logger=logger),
|
|
266
|
+
column_to_attr=UPS_COLUMNS_MAPPING,
|
|
267
|
+
name_attr=UPS_NAME_ATTR,
|
|
268
|
+
config=config,
|
|
269
|
+
logger=logger
|
|
270
|
+
)
|
|
271
|
+
ups_model = ups_factory.create_model(
|
|
272
|
+
url=ups_url,
|
|
273
|
+
cache_file_name=ups_cache_file,
|
|
274
|
+
table_id=ups_table_id,
|
|
275
|
+
force_download=False,
|
|
276
|
+
json_file_name=json_file_name,
|
|
277
|
+
model_kwargs={"dimse_mapping": UPS_DIMSE_MAPPING},
|
|
278
|
+
)
|
|
279
|
+
ups_model.select_dimse(args.dimse)
|
|
280
|
+
ups_model.select_role(args.role)
|
|
281
|
+
|
|
282
|
+
# --- Merge by path with DICOM service default type logic ---
|
|
283
|
+
|
|
284
|
+
# Use UPS_DIMSE_MAPPING to get the attributes to merge for the selected DIMSE
|
|
285
|
+
dimse_info = UPS_DIMSE_MAPPING.get(args.dimse, {})
|
|
286
|
+
dimse_attributes = dimse_info.get("attributes", [])
|
|
287
|
+
dimse_req_attributes = dimse_info.get("req_attributes", [])
|
|
288
|
+
# Add "comment" to the end of the list
|
|
289
|
+
dimse_attributes.append("comment")
|
|
290
|
+
|
|
291
|
+
merger = SpecMerger(config=config, logger=logger)
|
|
292
|
+
merged_model = merger.merge_path_with_default(
|
|
293
|
+
iod_model,
|
|
294
|
+
ups_model,
|
|
295
|
+
match_by="attribute",
|
|
296
|
+
attribute_name="elem_tag",
|
|
297
|
+
merge_attrs=dimse_attributes,
|
|
298
|
+
default_attr="elem_type",
|
|
299
|
+
default_value="3",
|
|
300
|
+
default_value_func=dicom_service_default_type,
|
|
301
|
+
ignore_module_level=True,
|
|
302
|
+
json_file_name=None # do not cache as more processing is necessary
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# --- replace the type with spec from the selected DIMSE and role ---
|
|
306
|
+
align_type_with_dimse_req(merged_model, dimse_req_attributes, dimse_attributes)
|
|
307
|
+
|
|
308
|
+
# --- Store the aligned model to JSON with _aligned suffix ---
|
|
309
|
+
|
|
310
|
+
# Build filename for the cached merged model
|
|
311
|
+
dimse_part = args.dimse.replace("-", "").replace(" ", "").upper() if args.dimse else "ALLDIMSE"
|
|
312
|
+
role_part = args.role.upper() if args.role else "ALLROLES"
|
|
313
|
+
merged_model_filename = f"UPSIOD_{dimse_part}_{role_part}.json"
|
|
314
|
+
|
|
315
|
+
json_store = JSONSpecStore(logger=logger)
|
|
316
|
+
merged_model_path = os.path.join(config.get_param("cache_dir"), "model", merged_model_filename)
|
|
317
|
+
json_store.save(merged_model, merged_model_path)
|
|
318
|
+
logger.info(f"Aligned model saved to {merged_model_path}")
|
|
319
|
+
|
|
320
|
+
# --- Print or use the merged model ---
|
|
321
|
+
printer = SpecPrinter(merged_model)
|
|
322
|
+
if args.print_mode == "tree":
|
|
323
|
+
printer.print_tree(colorize=True)
|
|
324
|
+
elif args.print_mode == "table":
|
|
325
|
+
printer.print_table(colorize=True)
|
|
326
|
+
# else: do not print anything if print_mode == "none"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
if __name__ == "__main__":
|
|
330
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# IOD Explorer
|
|
2
|
+
|
|
3
|
+
A GUI application for exploring DICOM specifications interactively.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
For complete documentation including installation, configuration, and usage instructions, see:
|
|
8
|
+
|
|
9
|
+
**[IOD Explorer Documentation](../../../../../docs/apps/iod-explorer.md)**
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
poetry run iod-explorer
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For more running options and configuration details, refer to the main documentation linked above.
|
|
18
|
+
|
|
19
|
+
## Dependencies
|
|
20
|
+
|
|
21
|
+
- **tkhtmlview** (installed automatically with Poetry)
|
|
22
|
+
- **tkinter** (required for the GUI, but not installed via pip or Poetry)
|
|
23
|
+
|
|
24
|
+
> **Note:**
|
|
25
|
+
> `tkinter` is part of the Python standard library, but on some Linux distributions and on macOS with Homebrew Python, it must be installed separately.
|
|
26
|
+
>
|
|
27
|
+
> - On **Ubuntu/Debian**: `sudo apt install python3-tk`
|
|
28
|
+
> - On **Fedora**: `sudo dnf install python3-tkinter`
|
|
29
|
+
> - On **macOS (Homebrew Python)**: `brew install tcl-tk`
|
|
30
|
+
> - You may also need to set environment variables so Python can find the Tk libraries. See [Homebrew Python and Tkinter](https://docs.brew.sh/Homebrew-and-Python#tkinter) for details.
|
|
31
|
+
> - On **Windows/macOS (python.org installer)**: Usually included with the official Python installer.
|
|
32
|
+
>
|
|
33
|
+
> If you get an error about `tkinter` not being found, please install it as shown above.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# IOD Explorer Configuration
|
|
2
|
+
|
|
3
|
+
This directory contains configuration files and documentation for the IOD Explorer GUI application.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Configuration File Search Order](#configuration-file-search-order)
|
|
8
|
+
- [Configuration Options](#configuration-options)
|
|
9
|
+
- [Example Configuration Files](#example-configuration-files)
|
|
10
|
+
- [Configuration Files in This Directory](#configuration-files-in-this-directory)
|
|
11
|
+
- [Quick Configuration Test](#quick-configuration-test)
|
|
12
|
+
- [Testing Configuration Priority](#testing-configuration-priority)
|
|
13
|
+
|
|
14
|
+
## Configuration File Search Order
|
|
15
|
+
|
|
16
|
+
The application searches for configuration files in the following priority order:
|
|
17
|
+
|
|
18
|
+
### Tier 1: App-Specific Configuration Files
|
|
19
|
+
|
|
20
|
+
1. `iod_explorer_config.json` in the current directory
|
|
21
|
+
2. `~/.config/dcmspec/iod_explorer_config.json` in the user config directory
|
|
22
|
+
3. `iod_explorer_config.json` in this app config directory (`src/dcmspec/apps/iod_explorer/config/`)
|
|
23
|
+
4. `iod_explorer_config.json` in the same directory as the script (legacy support)
|
|
24
|
+
|
|
25
|
+
### Tier 2: Base Library Configuration (Fallback)
|
|
26
|
+
|
|
27
|
+
If no app-specific config is found, the base `Config` class looks for:
|
|
28
|
+
|
|
29
|
+
- **macOS**: `~/Library/Application Support/iod_explorer/config.json`
|
|
30
|
+
- **Linux**: `~/.config/iod_explorer/config.json`
|
|
31
|
+
- **Windows**: `%USERPROFILE%\AppData\Local\iod_explorer\config.json`
|
|
32
|
+
|
|
33
|
+
### Default Behavior (No Config Files)
|
|
34
|
+
|
|
35
|
+
If no configuration files are found anywhere, the application uses:
|
|
36
|
+
|
|
37
|
+
- **Cache directory**: Platform-specific cache directory (e.g., `~/Library/Caches/iod_explorer`)
|
|
38
|
+
- **Log level**: INFO
|
|
39
|
+
|
|
40
|
+
## Configuration Options
|
|
41
|
+
|
|
42
|
+
### `cache_dir`
|
|
43
|
+
|
|
44
|
+
- **Type**: String
|
|
45
|
+
- **Default**: Platform-specific cache directory (e.g., `~/Library/Caches/iod_explorer` on macOS)
|
|
46
|
+
- **Description**: Directory to store downloaded DICOM specifications and cached models
|
|
47
|
+
|
|
48
|
+
### `log_level`
|
|
49
|
+
|
|
50
|
+
- **Type**: String
|
|
51
|
+
- **Default**: "INFO"
|
|
52
|
+
- **Valid values**: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
|
|
53
|
+
- **Description**: Sets the logging level for the application
|
|
54
|
+
|
|
55
|
+
## Example Configuration Files
|
|
56
|
+
|
|
57
|
+
### Default Configuration
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"cache_dir": "./cache",
|
|
62
|
+
"log_level": "INFO"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Debug Configuration (Verbose Logging)
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"cache_dir": "/tmp/debug_cache",
|
|
71
|
+
"log_level": "DEBUG"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Minimal Logging Configuration
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"cache_dir": "~/Documents/iod_explorer_cache",
|
|
80
|
+
"log_level": "WARNING"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration Files in This Directory
|
|
85
|
+
|
|
86
|
+
This directory contains several example configuration files which you can use as templates for your own configuration:
|
|
87
|
+
|
|
88
|
+
- **`iod_explorer_config.json`**: Default configuration with INFO logging
|
|
89
|
+
- **`iod_explorer_config_example.json`**: Basic example configuration
|
|
90
|
+
- **`iod_explorer_config_debug.json`**: Debug configuration with verbose logging
|
|
91
|
+
- **`iod_explorer_config_minimal_logging.json`**: Minimal logging configuration
|
|
92
|
+
|
|
93
|
+
To use a template:
|
|
94
|
+
|
|
95
|
+
1. Copy the desired config file to one of the search locations (see above)
|
|
96
|
+
2. Rename it to `iod_explorer_config.json`
|
|
97
|
+
3. Modify settings as needed
|
|
98
|
+
4. Start the IOD Explorer application
|
|
99
|
+
|
|
100
|
+
The application will automatically detect and use the configuration file. You'll see log messages indicating which configuration file was loaded and the current settings.
|
|
101
|
+
|
|
102
|
+
## Quick Configuration Test
|
|
103
|
+
|
|
104
|
+
You can test your configuration without running the full GUI:
|
|
105
|
+
|
|
106
|
+
> **Note:**
|
|
107
|
+
> Before running the command below, make sure you are in the root of your dcmspec project directory (`cd /path/to/dcmspec`).
|
|
108
|
+
> Also, if you are not using the default `.venv` virtual environment, replace `.venv/bin/python` with the path to your Python executable.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
.venv/bin/python -c "\
|
|
112
|
+
from dcmspec.apps.ui.iod_explorer.iod_explorer import load_app_config, setup_logger; \
|
|
113
|
+
config = load_app_config(); \
|
|
114
|
+
logger = setup_logger(config); \
|
|
115
|
+
print(); \
|
|
116
|
+
print(f'Config file: {config.config_file}'); \
|
|
117
|
+
print(f'Cache dir: {config.cache_dir}'); \
|
|
118
|
+
print(f'Log level: {config.get_param(\"log_level\")}'); \
|
|
119
|
+
logger.info('Test log message'); \
|
|
120
|
+
logger.debug('Debug message')"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Testing Configuration Priority
|
|
124
|
+
|
|
125
|
+
To test the configuration search order, run the following command to create a test config file and see which config file is detected and used:
|
|
126
|
+
|
|
127
|
+
> **Note:**
|
|
128
|
+
> Before running the command below, make sure you are in the root of your dcmspec project directory (`cd /path/to/dcmspec`).
|
|
129
|
+
> Also, if you are not using the default `.venv` virtual environment, replace `.venv/bin/python` with the path to your Python executable.
|
|
130
|
+
|
|
131
|
+
The command below will create a file named iod_explorer_config.json in your current directory.
|
|
132
|
+
You may want to delete this file after testing to avoid it taking precedence over other config files in future runs.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
echo '{"cache_dir": "./app_cache", "log_level": "INFO"}' > iod_explorer_config.json
|
|
136
|
+
|
|
137
|
+
.venv/bin/python -c "\
|
|
138
|
+
from dcmspec.apps.ui.iod_explorer.iod_explorer import load_app_config, setup_logger; \
|
|
139
|
+
config = load_app_config(); \
|
|
140
|
+
logger = setup_logger(config); \
|
|
141
|
+
print(); \
|
|
142
|
+
logger.info('Starting IOD Explorer'); \
|
|
143
|
+
log_level = config.get_param('log_level') or 'INFO'; \
|
|
144
|
+
source = 'app-specific' if 'iod_explorer_config.json' in (config.config_file or '') else 'default'; \
|
|
145
|
+
logger.info(f'Logging configured: level={log_level.upper()}, source={source}'); \
|
|
146
|
+
logger.info(f'Config file: {config.config_file or \"none (using defaults)\"}'); \
|
|
147
|
+
logger.info(f'Cache directory: {config.cache_dir}');"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
You should see output similar to the following:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
INFO - Starting IOD Explorer
|
|
154
|
+
INFO - Logging configured: level=INFO, source=app-specific
|
|
155
|
+
INFO - Config file: iod_explorer_config.json
|
|
156
|
+
INFO - Cache directory: ./app_cache
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
To clean up after testing, you can remove the file:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
rm iod_explorer_config.json
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The application will display important configuration information at startup, including:
|
|
166
|
+
|
|
167
|
+
- **Log level and source**: Whether config comes from app-specific file or defaults
|
|
168
|
+
- **Config file location**: Exact path to the configuration file being used
|
|
169
|
+
- **Cache directory**: Where downloaded specifications and models are stored
|
|
170
|
+
|
|
171
|
+
This information helps with troubleshooting and understanding the application's configuration.
|