Skip to content

Commit

Permalink
Merge pull request #86 from dwikler/refact-basescp
Browse files Browse the repository at this point in the history
Refact basescp
  • Loading branch information
sjswerdloff authored Dec 15, 2024
2 parents 9bd5b3f + d349790 commit f389961
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 37 deletions.
33 changes: 0 additions & 33 deletions tdwii_plus_examples/TDWII_PPVS_subscriber/basescp.py

This file was deleted.

2 changes: 1 addition & 1 deletion tdwii_plus_examples/TDWII_PPVS_subscriber/echoscp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tdwii_plus_examples/TDWII_PPVS_subscriber/ppvsscp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tdwii_plus_examples/TDWII_PPVS_subscriber/storescp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
44 changes: 44 additions & 0 deletions tdwii_plus_examples/basehandlers.py
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
137 changes: 137 additions & 0 deletions tdwii_plus_examples/basescp.py
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()
76 changes: 76 additions & 0 deletions tdwii_plus_examples/tests/test_basescp.py
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()

0 comments on commit f389961

Please sign in to comment.