-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #86 from dwikler/refact-basescp
Refact basescp
- Loading branch information
Showing
8 changed files
with
261 additions
and
37 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
def handle_open(event, logger): | ||
"""Handler for evt.EVT_CONN_OPEN | ||
Parameters | ||
---------- | ||
event : events.Event | ||
The corresponding event. | ||
logger : logging.Logger | ||
The application's logger. | ||
Returns | ||
------- | ||
int | ||
The status of possible processing of opened connection parameters, | ||
always ``0x0000`` (Success). | ||
""" | ||
requestor = event.assoc.requestor | ||
addr, port = requestor.address, requestor.port | ||
logger.info(f"Succesful connection from {addr}:{port}") | ||
|
||
return 0x0000 | ||
|
||
|
||
def handle_close(event, logger): | ||
"""Handler for evt.EVT_CONN_CLOSE | ||
Parameters | ||
---------- | ||
event : events.Event | ||
The corresponding event. | ||
logger : logging.Logger | ||
The application's logger. | ||
Returns | ||
------- | ||
int | ||
The status of possible processing of closed connection parameters, | ||
always ``0x0000`` (Success). | ||
""" | ||
requestor = event.assoc.requestor | ||
aet, addr, port = requestor.ae_title, requestor.address, requestor.port | ||
logger.info(f"Closed connection with {aet}@{addr}:{port}") | ||
|
||
return 0x0000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
from argparse import Namespace | ||
|
||
from pynetdicom import AE, evt | ||
from pynetdicom.apps.common import setup_logging | ||
|
||
from tdwii_plus_examples.basehandlers import handle_open, handle_close | ||
|
||
|
||
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 | ||
Application Entity (AE) and handling incoming associations. | ||
Usage: | ||
To use the BaseSCP class, create a subclass and override | ||
the [_add_contexts] and [_add_handlers] methods | ||
to add presentation contexts and handlers for specific DICOM services. | ||
Attributes: | ||
ae_title (str): The title of the Application Entity (AE) | ||
bind_address (str): The IP address or hostname of the AE | ||
port (int): The port number the AE will listen on | ||
logger: A logger instance (optional) | ||
Methods: | ||
_add_contexts(self) | ||
Adds presentation contexts to the SCP instance. | ||
_add_handlers(self) | ||
Adds handlers for DICOM communication events to the SCP instance. | ||
run(self) | ||
Starts the SCP AE thread. | ||
stop(self) | ||
Stops the SCP AE. | ||
""" | ||
def __init__(self, | ||
ae_title: str = "BASE_SCP", | ||
bind_address: str = "", | ||
port: int = 11112, | ||
logger=None): | ||
|
||
""" | ||
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) | ||
Optional, default: "BASE_SCP" | ||
bind_address : str | ||
A specific IP address or hostname of the AE | ||
Optional, default: "" will bind to all interfaces. | ||
port: int | ||
The port number to listen on | ||
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(self.logger, logging.Logger): | ||
# raise TypeError("logger must be an instance of logging.Logger") | ||
else: | ||
self.logger = logger | ||
|
||
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): | ||
raise TypeError("port must be an integer") | ||
self.port = port | ||
|
||
if not isinstance(ae_title, str): | ||
raise TypeError("ae_title must be a string") | ||
self.ae_title = ae_title | ||
self.ae = AE(self.ae_title) | ||
|
||
self._add_contexts() | ||
|
||
self.handlers = [] | ||
self._add_handlers() | ||
|
||
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])) | ||
self.handlers.append((evt.EVT_CONN_CLOSE, handle_close, | ||
[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), | ||
evt_handlers=self.handlers, | ||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import unittest | ||
import logging | ||
from logging.handlers import MemoryHandler | ||
import subprocess | ||
import time | ||
from tdwii_plus_examples.basescp import BaseSCP | ||
from pynetdicom.sop_class import Verification | ||
|
||
|
||
class EchoSCP(BaseSCP): | ||
def __init__(self, bind_address, logger=None): | ||
super().__init__(bind_address=bind_address, logger=logger) | ||
|
||
def _add_contexts(self): | ||
super()._add_contexts() | ||
self.ae.add_supported_context(Verification, "1.2.840.10008.1.2") | ||
|
||
|
||
class TestBaseSCP(unittest.TestCase): | ||
|
||
def setUp(self): | ||
# Set up the logger for the BaseSCP to INFO level | ||
# 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.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 | ||
self.test_logger = logging.getLogger('test_basescp') | ||
self.test_logger.setLevel(logging.DEBUG) | ||
self.stream_handler = logging.StreamHandler() | ||
self.test_logger.addHandler(self.stream_handler) | ||
|
||
# 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']) | ||
|
||
# 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] | ||
|
||
# Check the EVT_CONN_OPEN event log message | ||
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: " | ||
f"{log_messages[-1]}") | ||
self.assertRegex(log_messages[-1], | ||
r"Closed connection with " + | ||
r"([\w]+)@" + | ||
r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):([\w]{3,5})") | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |