Application Entity¶
The first step in DICOM networking with pynetdicom is the creation of an
Application Entity by using the
AE
class. A minimal initialisation of
AE
requires no parameters:
>>> from pynetdicom import AE
>>> ae = AE()
This will create an AE
with an AE title of
'PYNETDICOM'
. The AE title can set by supplying the ae_title
keyword parameter during initialisation:
>>> from pynetdicom import AE
>>> ae = AE(ae_title='MY_AE_TITLE')
Or afterwards with the ae_title
property:
>>> from pynetdicom import AE
>>> ae = AE()
>>> ae.ae_title = 'MY_AE_TITLE'
AE titles must meet the conditions of a DICOM data element with a Value Representation of AE:
Leading and trailing spaces (hex
0x20
) are non-significant.Maximum 16 characters (once non-significant characters are removed).
Valid characters belong to the DICOM Default Character Repertoire, which is the basic G0 Set of the ISO/IEC 646:1991 (ASCII) standard excluding backslash (
\
- hex0x5C
) and all control characters (such as'\n'
).An AE title made entirely of spaces is not allowed.
When creating SCPs it’s also possible to give each SCP its own AE title through
the ae_title keyword parameter in
AE.start_server()
.
Creating an SCU¶
Adding requested Presentation Contexts¶
If you intend to use your AE as a Service Class User then you need to specify the Presentation Contexts that will be requested during Association negotiation. This can be done in two ways:
You can add requested contexts on a one-by-one basis using the
AE.add_requested_context()
method.You can set all the requested contexts at once using the
AE.requested_contexts
property. Additional contexts can still be added on a one-by-one basis afterwards.
Adding presentation contexts one-by-one:
>>> from pydicom.uid import UID
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> ae.add_requested_context('1.2.840.10008.1.1')
>>> ae.add_requested_context(UID('1.2.840.10008.5.1.4.1.1.4'))
>>> ae.add_requested_context(CTImageStorage)
Adding presentation contexts all at once:
>>> from pynetdicom import AE, StoragePresentationContexts
>>> ae = AE()
>>> ae.requested_contexts = StoragePresentationContexts
Here StoragePresentationContexts
is a
prebuilt list of presentation contexts containing 120 of the most commonly used Storage
Service Classes’ supported SOP Classes,
and there’s a similar list for all
the supported service classes.
Alternatively you can build your own list of presentation contexts, either
through creating new PresentationContext
instances or by using the build_context()
convenience function:
>>> from pynetdicom import AE, build_context
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> contexts = [
... build_context(CTImageStorage),
... build_context('1.2.840.10008.1.1')
... ]
>>> ae.requested_contexts = contexts
Combining the all-at-once and one-by-one approaches:
>>> from pynetdicom import AE, StoragePresentationContexts
>>> from pynetdicom.sop_class import Verification
>>> ae = AE()
>>> ae.requested_contexts = StoragePresentationContexts
>>> ae.add_requested_context(Verification)
As the association Requestor you’re limited to a total of 128 requested
presentation contexts, so attempting to add more than 128 contexts will raise
a ValueError
exception. StoragePresentationContexts
consists of 120 of most commonly used Storage
Service Classes, therefore you are able to add 8 additional presentation contexts without rasing a ValueError
exception.
When you add presentation contexts as shown above, the following transfer syntaxes are used by default for each context:
1.2.840.10008.1.2 |
Implicit VR Little Endian |
1.2.840.10008.1.2.1 |
Explicit VR Little Endian |
1.2.840.10008.1.2.1.99 |
Deflated Explicit VR Little Endian |
1.2.840.10008.1.2.2 |
Explicit VR Big Endian |
Specifying your own transfer syntax(es) can be done with the transfer_syntax keyword parameter as either a single str/UID or a list of str/UIDs:
>>> from pydicom.uid import ImplicitVRLittleEndian, ExplicitVRLittleEndian
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import CTImageStorage, MRImageStorage
>>> ae = AE()
>>> ae.add_requested_context(CTImageStorage, transfer_syntax='1.2.840.10008.1.2')
>>> ae.add_requested_context(
... MRImageStorage, [ImplicitVRLittleEndian, ExplicitVRLittleEndian]
... )
>>> from pydicom.uid import ImplicitVRLittleEndian, ExplicitVRBigEndian
>>> from pynetdicom import AE, build_context
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> context_a = build_context(CTImageStorage, ImplicitVRLittleEndian)
>>> context_b = build_context(
... '1.2.840.10008.1.1', [ImplicitVRLittleEndian, ExplicitVRBigEndian]
... )
>>> ae.requested_contexts = [context_a, context_b]
The requested presentation contexts can be accessed with the
AE.requested_contexts
property and are returned in the order they were added.
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import Verification
>>> ae = AE()
>>> len(ae.requested_contexts)
0
>>> ae.add_requested_context(Verification)
>>> len(ae.requested_contexts)
1
>>> print(ae.requested_contexts[0])
Abstract Syntax: Verification SOP Class
Transfer Syntax(es):
=Implicit VR Little Endian
=Explicit VR Little Endian
=Explicit VR Big Endian
It’s also possible to have multiple requested presentation contexts for the same abstract syntax.
>>> from pydicom.uid import ImplicitVRLittleEndian
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> ae.add_requested_context(CTImageStorage)
>>> ae.add_requested_context(CTImageStorage, ImplicitVRLittleEndian)
>>> len(ae.requested_contexts)
2
>>> print(ae.requested_contexts[0])
Abstract Syntax: CT Image Storage
Transfer Syntax(es):
=Implicit VR Little Endian
=Explicit VR Little Endian
=Explicit VR Big Endian
>>> print(ae.requested_contexts[1])
Abstract Syntax: CT Image Storage
Transfer Syntax(es):
=Implicit VR Little Endian
All the above examples set the requested presentation contexts on the
Application Entity level, i.e. the same contexts will be used for all
association requests. To set the requested presentation contexts on a
per-association basis (i.e. each association request can have different
requested contexts) you can use the contexts keyword parameter when calling
AE.associate()
(see
the Association page for more information).
Specifying the network port¶
In general it shouldn’t be necessary to specify the port when acting as an SCU,
as by default pynetdicom will use the first port available. To specify the
port number manually you can use the bind_address keyword parameter when
requesting an association, which takes a 2-tuple of (str
host,
int
port):
>>> from pynetdicom import AE
>>> ae = AE()
>>> ae.add_requested_context('1.2.840.10008.1.1')
>>> assoc = ae.associate("127.0.0.1", 11112, bind_address=("127.0.0.1", 11113))
Note
When using bind_address you may sometimes be unable to immediately
reconnect with the same bound address and port due to an exception about
the socket or address already being in use. This occurs because
the previous TCP connection using the bound socket remains in a
TIME_WAIT
state which must expire before you are able to re-use the socket.
Association¶
For information on how request an association with a peer AE when acting as an SCU please see the Association page.
Creating an SCP¶
Adding supported Presentation Contexts¶
If you intend to use your AE as a Service Class Provider then you need to specify the Presentation Contexts that will be supported during Association negotiation. This can be done in two ways:
You can add supported contexts on a one-by-one basis using the
AE.add_supported_context()
method.You can set all the supported contexts at once using the
AE.supported_contexts
property. Additional contexts can still be added on a one-by-one basis afterwards.
Adding presentation contexts one-by-one:
>>> from pydicom.uid import UID
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> ae.add_supported_context('1.2.840.10008.1.1')
>>> ae.add_supported_context(UID('1.2.840.10008.5.1.4.1.1.4'))
>>> ae.add_supported_context(CTImageStorage)
Adding presentation contexts all at once:
>>> from pynetdicom import AE, StoragePresentationContexts
>>> ae = AE()
>>> ae.supported_contexts = StoragePresentationContexts
Here StoragePresentationContexts
is a prebuilt
list
of presentation contexts containing 120 of the most commonly used Storage
Service Classes’ supported SOP Classes,
and there’s a similar list for
all the supported service classes. Alternatively you can build your own list
of presentation contexts, either through creating new
PresentationContext
instances or by using the
build_context()
convenience function:
>>> from pynetdicom import AE, build_context
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> contexts = [
... build_context(CTImageStorage),
... build_context('1.2.840.10008.1.1')
... ]
>>> ae.supported_contexts = contexts
Combining the all-at-once and one-by-one approaches:
>>> from pynetdicom import AE, AllStoragePresentationContexts
>>> from pynetdicom.sop_class import Verification
>>> ae = AE()
>>> ae.supported_contexts = AllStoragePresentationContexts
>>> ae.add_supported_context(Verification)
As the association Acceptor you’re not limited in the number of presentation contexts that you can support.
When you add presentation contexts as shown above, the following transfer syntaxes are used by default for each context:
1.2.840.10008.1.2 |
Implicit VR Little Endian |
1.2.840.10008.1.2.1 |
Explicit VR Little Endian |
1.2.840.10008.1.2.1.99 |
Deflated Explicit VR Little Endian |
1.2.840.10008.1.2.2 |
Explicit VR Big Endian |
Specifying your own transfer syntax(es) can be done with the transfer_syntax keyword parameter as either a single str/UID or a list of str/UIDs:
>>> from pydicom.uid import ImplicitVRLittleEndian, ExplicitVRLittleEndian
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import CTImageStorage, MRImageStorage
>>> ae = AE()
>>> ae.add_supported_context(CTImageStorage, transfer_syntax='1.2.840.10008.1.2')
>>> ae.add_supported_context(
... MRImageStorage, [ImplicitVRLittleEndian, ExplicitVRLittleEndian]
... )
>>> from pydicom.uid import ImplicitVRLittleEndian, ExplicitVRBigEndian
>>> from pynetdicom import AE, build_context
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> context_a = build_context(CTImageStorage, ImplicitVRLittleEndian)
>>> context_b = build_context(
... '1.2.840.10008.1.1', [ImplicitVRLittleEndian, ExplicitVRBigEndian]
... )
>>> ae.supported_contexts = [context_a, context_b]
The supported presentation contexts can be accessed with the
AE.supported_contexts
property and are returned in order of their abstract syntax UID value:
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import Verification
>>> ae = AE()
>>> len(ae.supported_contexts)
0
>>> ae.add_supported_context(Verification)
>>> len(ae.supported_contexts)
1
>>> print(ae.supported_contexts[0])
Abstract Syntax: Verification SOP Class
Transfer Syntax(es):
=Implicit VR Little Endian
=Explicit VR Little Endian
=Explicit VR Big Endian
For the association Acceptor it’s not possible to have multiple supported presentation contexts for the same abstract syntax, instead any additional transfer syntaxes will be combined with the pre-existing context:
>>> from pydicom.uid import ImplicitVRLittleEndian, ExplicitVRLittleEndian
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> ae.add_supported_context(CTImageStorage, ExplicitVRLittleEndian)
>>> ae.add_supported_context(CTImageStorage, ImplicitVRLittleEndian)
>>> len(ae.supported_contexts)
1
>>> print(ae.supported_contexts[0])
Abstract Syntax: CT Image Storage
Transfer Syntax(es):
=Explicit VR Little Endian
=Implicit VR Little Endian
All the above examples set the supported presentation contexts on the
Application Entity level, i.e. the same contexts will be used for all
SCPs. To set the supported presentation contexts on a
per-SCP basis (i.e. each SCP can have different
supported contexts) you can use the contexts keyword parameter when calling
AE.start_server()
(see
the Association page for more information).
Handling SCP/SCU Role Selection Negotiation¶
Depending on the requirements of the service class, an association Requestor may include SCP/SCU Role Selection Negotiation items in the association request and it’s up to the association Acceptor to decide whether or not to accept the proposed roles. This can be done through the scu_role and scp_role keyword parameters, which control whether or not the association Acceptor will accept or reject the proposal:
>>> from pynetdicom import AE
>>> from pynetdicom.sop_class import CTImageStorage
>>> ae = AE()
>>> ae.add_supported_context(CTImageStorage, scu_role=False, scp_role=True)
If either scu_role or scp_role is None
(the default) then no response
to the role selection will be sent and the default roles assumed. If you wish
to accept a proposed role then set the corresponding parameter to True
. In
the above example if the Requestor proposes the SCP role then the Acceptor
will accept it while rejecting the proposed SCU role (and therefore the
Acceptor will be SCU and Requestor SCP).
To reiterate, if you wish to respond to the proposed role selection then both scu_role and scp_role must be set, and the value you set indicates whether or not to accept or reject the proposal.
There are four possible outcomes for the role selection negotiation, depending on what was proposed and what was accepted:
The proposed roles aren’t acceptable and the context is rejected
The Acceptor acts as the SCP and the Requestor the SCU (default)
The Acceptor acts as the SCU and the Requestor the SCP
Both Acceptor and Requestor act as SCU and SCP
Handling User Identity Negotiation¶
An association Requestor may include a User Identity Negotiation item in the association request with the aim of providing the Acceptor a method of verifying its identity. Possible forms of identity confirmation methods are:
Username
Username and password (sent in the clear)
Kerberos service ticket
SAML assertion
JSON web token
By default, all association requests that include user identity negotiation are accepted (provided there’s no other reason to reject) and no user identity negotiation response is sent even if one is requested.
To handle a user identity negotiation yourself you
should implement and bind a handler to the
evt.EVT_USER_ID
event. Check the
documentation
to see the requirements for implementations of the evt.EVT_USER_ID
handler.
from pynetdicom import AE, evt
from pynetdicom.sop_class import Verification
from my_code import some_user_function
def handle_user_id(event):
"""Handle evt.EVT_USER_ID."""
# Identity verification code is outside the scope of pynetdicom
is_verified, response = some_user_function(event)
return is_verified, response
handlers = [(evt.EVT_USER_ID, handle_user_id)]
ae = AE()
ae.add_supported_context(Verification)
ae.start_server(("127.0.0.1", 11112), evt_handlers=handlers)
Specifying the bind address¶
The bind address for the server socket is specified by the address
parameter to start_server()
as
(str host, int port).
>>> from pynetdicom import AE
>>> ae = AE()
>>> ae.add_supported_context('1.2.840.10008.1.1')
>>> ae.start_server(("127.0.0.1", 11112))
Association¶
For information on how to start listening for association requests from peer AEs and how to handle their service requests see the Association page.
References¶
DICOM Standard, Part 5 Section 6.2
DICOM Standard, Part 5 Section 6.1.2.1
DICOM Standard, Part 5 Section 6.1.3
DICOM Standard, Part 8 Section 9.3.2
DICOM Standard, Part 8 Annex B.1
DICOM Standard, Part 8 Annex B.2