From d34979091cda58a19041a8f5838cd2b2e07deb21 Mon Sep 17 00:00:00 2001 From: David Wikler Date: Sat, 14 Dec 2024 14:17:02 +0100 Subject: [PATCH] Replace obsolete BaseSCP --- .../TDWII_PPVS_subscriber/basescp.py | 33 ----------- .../TDWII_PPVS_subscriber/echoscp.py | 2 +- .../TDWII_PPVS_subscriber/nevent_receiver.py | 2 +- .../TDWII_PPVS_subscriber/ppvsscp.py | 2 +- .../TDWII_PPVS_subscriber/storescp.py | 2 +- tdwii_plus_examples/basescp.py | 55 +++++++++++++------ tdwii_plus_examples/tests/test_basescp.py | 50 +++++++++-------- 7 files changed, 69 insertions(+), 77 deletions(-) delete mode 100644 tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py deleted file mode 100644 index 1e83626..0000000 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py +++ /dev/null @@ -1,33 +0,0 @@ -from argparse import Namespace - -from pynetdicom import AE -from pynetdicom.apps.common import setup_logging - - -class BaseSCP: - def __init__(self, ae_title: str = "BASE_SCP", port: int = 11112, logger=None, bind_address: str = ""): - self.ae_title = ae_title - self.port = port - if logger is None: - logger_args = Namespace(log_type="d", log_level="debug") - self.logger = setup_logging(logger_args, "base_scp") - else: - self.logger = logger - self.bind_address = bind_address - self.threaded_server = None - self.ae = AE(self.ae_title) - self.handlers = [] - self._add_contexts() - self._add_handlers() - - def _add_contexts(self): - # self.ae.add_supported_context(Verification, ALL_TRANSFER_SYNTAXES) - pass # base class, do nothing, pure virtual - - def _add_handlers(self): - # self.handlers.append((evt.EVT_C_ECHO, handle_echo, [None, self.logger])) - pass # base class, do nothing, pure virtual - - def run(self): - # Listen for incoming association requests - self.threaded_server = self.ae.start_server((self.bind_address, self.port), evt_handlers=self.handlers, block=False) diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py index 5c7cd05..59d6758 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py @@ -3,7 +3,7 @@ from pynetdicom import ALL_TRANSFER_SYNTAXES, evt from pynetdicom.sop_class import Verification -from tdwii_plus_examples.TDWII_PPVS_subscriber.basescp import BaseSCP +from tdwii_plus_examples.basescp import BaseSCP from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver_handlers import ( handle_echo, ) diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py index 8b267c9..dfc0ff2 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/nevent_receiver.py @@ -2,7 +2,7 @@ from pynetdicom import ALL_TRANSFER_SYNTAXES, UnifiedProcedurePresentationContexts, evt -from tdwii_plus_examples.TDWII_PPVS_subscriber.basescp import BaseSCP +from tdwii_plus_examples.basescp import BaseSCP from tdwii_plus_examples.TDWII_PPVS_subscriber.echoscp import EchoSCP from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver_handlers import ( handle_nevent, diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py index 1668211..c761741 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py @@ -4,7 +4,7 @@ from pynetdicom import ALL_TRANSFER_SYNTAXES, AllStoragePresentationContexts from tdwii_plus_examples import tdwii_config -from tdwii_plus_examples.TDWII_PPVS_subscriber.basescp import BaseSCP +from tdwii_plus_examples.basescp import BaseSCP from tdwii_plus_examples.TDWII_PPVS_subscriber.nevent_receiver import NEventReceiver from tdwii_plus_examples.TDWII_PPVS_subscriber.storescp import StoreSCP diff --git a/tdwii_plus_examples/TDWII_PPVS_subscriber/storescp.py b/tdwii_plus_examples/TDWII_PPVS_subscriber/storescp.py index 0fb3fcc..91cdac7 100644 --- a/tdwii_plus_examples/TDWII_PPVS_subscriber/storescp.py +++ b/tdwii_plus_examples/TDWII_PPVS_subscriber/storescp.py @@ -4,7 +4,7 @@ from pynetdicom import ALL_TRANSFER_SYNTAXES, AllStoragePresentationContexts, evt -from tdwii_plus_examples.TDWII_PPVS_subscriber.basescp import BaseSCP +from tdwii_plus_examples.basescp import BaseSCP from tdwii_plus_examples.TDWII_PPVS_subscriber.cstore_handler import handle_store from tdwii_plus_examples.TDWII_PPVS_subscriber.echoscp import EchoSCP diff --git a/tdwii_plus_examples/basescp.py b/tdwii_plus_examples/basescp.py index 485624e..e634834 100644 --- a/tdwii_plus_examples/basescp.py +++ b/tdwii_plus_examples/basescp.py @@ -1,9 +1,7 @@ from argparse import Namespace -import logging -from pynetdicom import AE, evt, ALL_TRANSFER_SYNTAXES +from pynetdicom import AE, evt from pynetdicom.apps.common import setup_logging -from pynetdicom.sop_class import Verification from tdwii_plus_examples.basehandlers import handle_open, handle_close @@ -13,7 +11,7 @@ class BaseSCP: The BaseSCP class is a base class for DICOM Service Class Providers (SCPs). This class provides basic functionality for creating and managing - a DICOM Service Class Provider (SCP) including setting up the + a DICOM Service Class Provider (SCP) including setting up the Application Entity (AE) and handling incoming associations. Usage: @@ -44,38 +42,39 @@ def __init__(self, logger=None): """ - Initializes a new instance of the BaseSCP class. + Initializes a new instance of the BaseSCP class. This method creates an AE without presentation contexts. Parameters ---------- ae_title : str The title of the Application Entity (AE) - Required, default: "BASE_SCP" + Optional, default: "BASE_SCP" bind_address : str - The IP address or hostname of the AE - Required, default: "" + A specific IP address or hostname of the AE + Optional, default: "" will bind to all interfaces. port: int The port number to listen on - Required, default: 11112 (as registered for DICOM at IANA) + Optional, default: 11112 (as registered for DICOM at IANA) logger: logging.Logger A logger instance Optional, default: None, a debug logger will be used. """ if logger is None: - self.logger = setup_logging(Namespace(log_type="d", - log_level="debug"), - "base_scp") - elif not isinstance(logger, logging.Logger): - raise TypeError("logger must be an instance of logging.Logger") + self.logger = setup_logging( + Namespace(log_type="d", log_level="debug"), "base_scp") + # elif not isinstance(self.logger, logging.Logger): + # raise TypeError("logger must be an instance of logging.Logger") else: self.logger = logger - if not isinstance(bind_address, str) or not bind_address.strip(): - raise ValueError("bind_address must be a non-empty string") + if not isinstance(bind_address, str): + raise ValueError("bind_address must be a string") + if not bind_address: + self.logger.debug("bind_address empty, binding to all interfaces") self.bind_address = bind_address if not isinstance(port, int): @@ -95,8 +94,19 @@ def __init__(self, self.threaded_server = None def _add_contexts(self): + """ + Adds DICOM presentation contexts to the SCP class. + + This method is intended to be overridden in derived classes. + """ pass # base class, do nothing, pure virtual + def _add_handlers(self): + """ + Adds handlers for processing DICOM events from incoming associations. + + This method is intended to be overridden in derived classes. + """ # To define actions when a TCP connection in opened or closed self.handlers.append((evt.EVT_CONN_OPEN, handle_open, [self.logger])) @@ -104,6 +114,13 @@ def _add_handlers(self): [self.logger])) def run(self): + """ + Start the SCP server and listen for incoming association requests + + This method starts an SCP server and begins listening for incoming + association requests. The server is run in a separate thread and will + not block the calling thread. + """ # Listen for incoming association requests self.threaded_server = self.ae.start_server( (self.bind_address, self.port), @@ -111,4 +128,10 @@ def run(self): block=False) def stop(self): + """ + Stop the SCP server. + + This method shuts down the SCP server by terminating any active + association servers and thread of the AE. + """ self.ae.shutdown() diff --git a/tdwii_plus_examples/tests/test_basescp.py b/tdwii_plus_examples/tests/test_basescp.py index b848fc8..4cb471e 100644 --- a/tdwii_plus_examples/tests/test_basescp.py +++ b/tdwii_plus_examples/tests/test_basescp.py @@ -4,8 +4,6 @@ import subprocess import time from tdwii_plus_examples.basescp import BaseSCP -from tdwii_plus_examples.basehandlers import handle_open -from pynetdicom import ALL_TRANSFER_SYNTAXES from pynetdicom.sop_class import Verification @@ -14,8 +12,9 @@ def __init__(self, bind_address, logger=None): super().__init__(bind_address=bind_address, logger=logger) def _add_contexts(self): - BaseSCP._add_contexts(self) - self.ae.add_supported_context(Verification, ALL_TRANSFER_SYNTAXES) + super()._add_contexts() + self.ae.add_supported_context(Verification, "1.2.840.10008.1.2") + class TestBaseSCP(unittest.TestCase): @@ -24,51 +23,54 @@ def setUp(self): # with a memory handler to store up to 100 log messages self.scp_logger = logging.getLogger('basescp') self.scp_logger.setLevel(logging.INFO) - self.memory_handler = MemoryHandler(100) + self.memory_handler = MemoryHandler(100) self.scp_logger.addHandler(self.memory_handler) - - # Set up the logger for this test to DEBUG level - # with a stream handler to print the log messages to the console + + # Set up the logger for this test to DEBUG level + # with a stream handler to print the log messages to the console self.test_logger = logging.getLogger('test_basescp') self.test_logger.setLevel(logging.DEBUG) - self.stream_handler = logging.StreamHandler() + self.stream_handler = logging.StreamHandler() self.test_logger.addHandler(self.stream_handler) - # Create the SCP - #self.scp = BaseSCP(bind_address="localhost", logger=self.scp_logger) + # Create a subclass of BaseSCP with only 1 presentation context: + # the required Verification presentation context with the + # default DICOM transfer syntax (Implicit VR Little Endian) self.scp = EchoSCP(bind_address="localhost", logger=self.scp_logger) - def test_run_and_check_log(self): # Run the SCP self.scp.run() # Send an echo request using pynetdicom's echoscu.py - subprocess.check_call(['python', '-m', 'pynetdicom', 'echoscu', - 'localhost', '11112', - '-aet', 'ECHOSCU', '-aec', 'BASE_SCP']) + subprocess.check_call(['python', '-m', 'pynetdicom', 'echoscu', + 'localhost', '11112', + '-aet', 'ECHOSCU', '-aec', 'BASE_SCP']) # Wait for 1 second to ensure the logs are generated time.sleep(1) # Get the log messages - log_messages = [record.getMessage() for record - in self.memory_handler.buffer] - + log_messages = [record.getMessage() for record + in self.memory_handler.buffer] + # Check the EVT_CONN_OPEN event log message - self.test_logger.info(f"Checking EVT_CONN_OPEN event log message: {log_messages[0]}") - self.assertRegex(log_messages[0], + self.test_logger.info(f"Checking EVT_CONN_OPEN event log message: " + f"{log_messages[0]}") + self.assertRegex(log_messages[0], r"Succesful connection from " + r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):([\w]{3,5})") # Stop the SCP self.scp.stop() # Check the EVT_CONN_CLOSED event log message - self.test_logger.info(f"Checking EVT_CONN_CLOSE event log message: {log_messages[-1]}") - self.assertRegex(log_messages[-1], + self.test_logger.info(f"Checking EVT_CONN_CLOSE event log message: " + f"{log_messages[-1]}") + self.assertRegex(log_messages[-1], r"Closed connection with " + - r"([\w]+)@"+ + r"([\w]+)@" + r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):([\w]{3,5})") + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main()