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.
Files changed (45) hide show
  1. dcmspec/__init__.py +0 -0
  2. dcmspec/apps/__init__.py +1 -0
  3. dcmspec/apps/cli/__init__.py +0 -0
  4. dcmspec/apps/cli/dataelements.py +89 -0
  5. dcmspec/apps/cli/iodattributes.py +125 -0
  6. dcmspec/apps/cli/iodmodules.py +92 -0
  7. dcmspec/apps/cli/modattributes.py +265 -0
  8. dcmspec/apps/cli/tdwiicontent.py +365 -0
  9. dcmspec/apps/cli/uidvalues.py +84 -0
  10. dcmspec/apps/cli/upsdimseattributes.py +109 -0
  11. dcmspec/apps/cli/upsioddimseattributes.py +330 -0
  12. dcmspec/apps/ui/iod_explorer/README.md +33 -0
  13. dcmspec/apps/ui/iod_explorer/__init__.py +9 -0
  14. dcmspec/apps/ui/iod_explorer/config/README.md +171 -0
  15. dcmspec/apps/ui/iod_explorer/config/iod_explorer_config.json +4 -0
  16. dcmspec/apps/ui/iod_explorer/config/iod_explorer_config_debug.json +4 -0
  17. dcmspec/apps/ui/iod_explorer/config/iod_explorer_config_example.json +4 -0
  18. dcmspec/apps/ui/iod_explorer/config/iod_explorer_config_minimal_logging.json +4 -0
  19. dcmspec/apps/ui/iod_explorer/iod_explorer.py +989 -0
  20. dcmspec/config.py +90 -0
  21. dcmspec/csv_table_spec_parser.py +85 -0
  22. dcmspec/doc_handler.py +214 -0
  23. dcmspec/dom_table_spec_parser.py +831 -0
  24. dcmspec/dom_utils.py +116 -0
  25. dcmspec/iod_spec_builder.py +444 -0
  26. dcmspec/iod_spec_printer.py +59 -0
  27. dcmspec/json_spec_store.py +110 -0
  28. dcmspec/module_registry.py +51 -0
  29. dcmspec/pdf_doc_handler.py +451 -0
  30. dcmspec/progress.py +232 -0
  31. dcmspec/service_attribute_defaults.py +124 -0
  32. dcmspec/service_attribute_model.py +231 -0
  33. dcmspec/spec_factory.py +451 -0
  34. dcmspec/spec_merger.py +536 -0
  35. dcmspec/spec_model.py +461 -0
  36. dcmspec/spec_parser.py +37 -0
  37. dcmspec/spec_printer.py +131 -0
  38. dcmspec/spec_store.py +50 -0
  39. dcmspec/ups_xhtml_doc_handler.py +117 -0
  40. dcmspec/xhtml_doc_handler.py +182 -0
  41. dcmspec-0.2.1.dist-info/METADATA +139 -0
  42. dcmspec-0.2.1.dist-info/RECORD +45 -0
  43. dcmspec-0.2.1.dist-info/WHEEL +4 -0
  44. dcmspec-0.2.1.dist-info/entry_points.txt +11 -0
  45. 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,9 @@
1
+ """IOD Explorer application package.
2
+
3
+ This package contains the IOD Explorer GUI application for browsing
4
+ DICOM specifications interactively.
5
+ """
6
+
7
+ from .iod_explorer import main, IODExplorer
8
+
9
+ __all__ = ['main', 'IODExplorer']
@@ -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.
@@ -0,0 +1,4 @@
1
+ {
2
+ "cache_dir": "./cache",
3
+ "log_level": "INFO"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "cache_dir": "/tmp/dcmspec_debug_cache",
3
+ "log_level": "DEBUG"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "cache_dir": "./cache",
3
+ "log_level": "INFO"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "cache_dir": "~/Documents/dcmspec_cache",
3
+ "log_level": "WARNING"
4
+ }