[med-svn] [odil] 01/06: Imported Upstream version 0.5.0

Julien Lamy lamy-guest at moszumanska.debian.org
Wed Apr 13 16:12:54 UTC 2016


This is an automated email from the git hooks/post-receive script.

lamy-guest pushed a commit to branch master
in repository odil.

commit a4169f13b1c79d24a3e68c3ac4e3f7b5e0b31452
Author: Julien Lamy <lamy at unistra.fr>
Date:   Fri Apr 8 18:25:30 2016 +0200

    Imported Upstream version 0.5.0
---
 .travis.yml                                   |  23 ++-
 CMakeLists.txt                                |  12 +-
 applications/CMakeLists.txt                   |   4 +
 applications/echo.py                          |  38 ++++
 applications/find.py                          |  85 +++++++++
 applications/odil                             |  41 +++++
 applications/print_.py                        |  92 ++++++++++
 applications/transcode.py                     |  59 +++++++
 appveyor.yml                                  |   4 +-
 examples/CMakeLists.txt                       |  14 +-
 examples/convert.py                           |  48 ++++++
 examples/dump.py                              |  69 ++++++++
 examples/find.cpp                             |   6 +-
 examples/find.py                              |  40 +++++
 examples/store.cpp                            |   6 +-
 src/CMakeLists.txt                            |   9 +-
 src/odil/AssociationParameters.cpp            |  56 ++++++
 src/odil/AssociationParameters.h              |   9 +
 src/odil/BasicDirectoryCreator.cpp            |   2 +-
 src/odil/{SCU.cpp => EchoSCU.cpp}             |  32 +---
 src/odil/EchoSCU.h                            |  35 ++++
 src/odil/ElementsDictionary.cpp               |  24 +++
 src/odil/ElementsDictionary.h                 |   7 +-
 src/odil/Reader.cpp                           | 122 +++++++++----
 src/odil/Reader.h                             |   2 +
 src/odil/SCU.cpp                              |  22 ---
 src/odil/SCU.h                                |   3 -
 src/odil/UIDsDictionary.h                     |   4 +-
 src/odil/VR.cpp                               |   2 +-
 src/odil/Value.h                              |   2 +-
 src/odil/Writer.cpp                           |  99 +++++++----
 src/odil/Writer.h                             |   2 +
 src/odil/dcmtk/conversion.cpp                 |  24 ++-
 src/odil/json_converter.cpp                   |  18 +-
 src/odil/xml_converter.cpp                    |  33 ++--
 tests/CMakeLists.txt                          |  17 +-
 tests/code/Association.cpp                    |  38 ----
 tests/code/DataSet.cpp                        |   5 +-
 tests/code/EchoSCU.cpp                        |  28 +++
 tests/code/Reader.cpp                         |   6 +-
 tests/code/SCU.cpp                            |   5 -
 tests/code/Writer.cpp                         |   6 +-
 tests/code/json_converter.cpp                 |   4 +-
 tests/code/xml_converter.cpp                  |  15 +-
 tests/peer_fixture_base.py                    |  18 ++
 tests/run                                     | 139 +++++++++++++++
 tests/run.sh                                  |  57 ------
 tests/tools/CMakeLists.txt                    |   5 +-
 tests/wrappers/test_association.py            |  48 ++++++
 tests/wrappers/test_association_parameters.py | 116 +++++++++++++
 tests/wrappers/test_data_set.py               | 166 ++++++++++++++++++
 tests/wrappers/test_echo_scu.py               |  26 +++
 tests/wrappers/test_element.py                |  55 ++++++
 tests/wrappers/test_elements_dictionary.py    |  48 ++++++
 tests/wrappers/test_find_scu.py               |  51 ++++++
 tests/wrappers/test_get_scu.py                |  54 ++++++
 tests/wrappers/test_move_scu.py               |  65 +++++++
 tests/wrappers/test_registry.py               |  29 ++++
 tests/wrappers/test_store_scu.py              |  52 ++++++
 tests/wrappers/test_tag.py                    |  87 ++++++++++
 tests/wrappers/test_uid.py                    |  14 ++
 tests/wrappers/test_uids_dictionary.py        |  21 +++
 tests/wrappers/test_value.py                  |  88 ++++++++++
 tests/wrappers/test_vr.py                     |  12 ++
 wrappers/Assocation.cpp                       |  62 +++++++
 wrappers/AssocationParameters.cpp             | 240 ++++++++++++++++++++++++++
 wrappers/CMakeLists.txt                       |  19 ++
 wrappers/DataSet.cpp                          | 225 ++++++++++++++++++++++++
 wrappers/EchoSCU.cpp                          |  33 ++++
 wrappers/Element.cpp                          |  63 +++++++
 wrappers/ElementsDictionary.cpp               |  96 +++++++++++
 wrappers/Exception.cpp                        |  25 +++
 wrappers/FindSCU.cpp                          |  50 ++++++
 wrappers/GetSCU.cpp                           |  50 ++++++
 wrappers/MoveSCU.cpp                          |  59 +++++++
 wrappers/StoreSCU.cpp                         |  33 ++++
 wrappers/Tag.cpp                              |  33 ++++
 wrappers/UIDsDictionary.cpp                   |  32 ++++
 wrappers/VR.cpp                               |  52 ++++++
 wrappers/Value.cpp                            | 110 ++++++++++++
 wrappers/json_converter.cpp                   |  44 +++++
 wrappers/odil.cpp                             |  58 +++++++
 wrappers/read.cpp                             |  48 ++++++
 wrappers/registry.cpp                         |  44 +++++
 wrappers/uid.cpp                              |  22 +++
 wrappers/write.cpp                            |  72 ++++++++
 wrappers/xml_converter.cpp                    |  45 +++++
 87 files changed, 3543 insertions(+), 295 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index ea77fb1..c11ff2a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,16 +17,22 @@ addons:
     - zlib1g-dev
     - libboost-dev
     - libboost-filesystem-dev
+    - libboost-python-dev
+    - libboost-regex-dev
     - libboost-test-dev
     - dcmtk
+    - ninja-build
+    - cmake
+    - pkg-config
 before_install:
   - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew update; fi
   # JSONCpp conflicts with json-c
   - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew uninstall json-c; fi
   # Boost is already installed with another version
-  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew unlink boost; brew install boost; fi
-  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew install dcmtk icu4c jsoncpp; fi
-  - pip install --user cpp-coveralls
+  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew unlink boost; brew install boost boost-python; fi
+  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew install dcmtk icu4c jsoncpp ninja; fi
+  - pip install --user cpp-coveralls nose
+  - export PATH=$(python -c 'import site; print(site.getuserbase())')/bin:${PATH}
 before_script:
   - export SRC_DIR=$PWD
   - mkdir build
@@ -34,11 +40,10 @@ before_script:
   - export BIN_DIR=$PWD
   - CMAKE_CXX_FLAGS="-std=c++11"
   - if [ "${CC}" = "gcc" ]; then CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} --coverage"; fi
-  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then CMAKE_ARGUMENTS="-D ICU_INCLUDE_DIR=/usr/local/opt/icu4c/include/ -D ICU_LIBRARY=/usr/local/opt/icu4c/lib/libicuuc.dylib"; fi
-  - cmake -D CMAKE_CXX_FLAGS:STRING="${CMAKE_CXX_FLAGS}" -D CMAKE_BUILD_TYPE:STRING=Debug ${CMAKE_ARGUMENTS} ../
+  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then export PKG_CONFIG_PATH=/usr/local/opt/icu4c/lib/pkgconfig; fi
+  - cmake -G Ninja -D CMAKE_CXX_FLAGS:STRING="${CMAKE_CXX_FLAGS}" -D CMAKE_BUILD_TYPE:STRING=Debug ../
 script:
-  - make -j $(nproc)
-  - export PATH=$PWD/tests/tools:$PATH
-  - ../tests/run.sh
+  - ninja
+  - ../tests/run --no-network
 after_success:
-  - if [ "${CC}" = "gcc" ]; then ${HOME}/.local/bin/coveralls --exclude examples --exclude tests --exclude-pattern '.*CMake[^/]+\.c(?:pp)?' --exclude-pattern "/usr/.*" --root=${SRC_DIR} --build-root ${BIN_DIR} | grep -vP "^File '.*'$" | grep -vP ":creating '.*'$" | grep -vP "^Lines executed:.*" | sed '/^$/d'; fi
+  - if [ "${CC}" = "gcc" ]; then coveralls --exclude examples --exclude tests --exclude-pattern '.*CMake[^/]+\.c(?:pp)?' --exclude-pattern "/usr/.*" --root=${SRC_DIR} --build-root ${BIN_DIR} > /dev/null; fi
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1b5dfdc..05d609f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,17 +2,19 @@ cmake_minimum_required(VERSION 2.8)
 
 project("odil")
 set(odil_MAJOR_VERSION 0)
-set(odil_MINOR_VERSION 4)
-set(odil_PATCH_VERSION 2)
-set(odil_VERSION 
+set(odil_MINOR_VERSION 5)
+set(odil_PATCH_VERSION 0)
+set(odil_VERSION
     ${odil_MAJOR_VERSION}.${odil_MINOR_VERSION}.${odil_PATCH_VERSION})
 
 option(BUILD_EXAMPLES "Build the examples directory." ON)
+option(BUILD_WRAPPERS "Build the Python Wrappers." ON)
 
 set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}" ${CMAKE_MODULE_PATH})
 include(CTest)
 
 add_subdirectory("src")
+add_subdirectory("applications")
 
 if(BUILD_EXAMPLES)
     add_subdirectory("examples")
@@ -22,6 +24,10 @@ if(BUILD_TESTING)
     add_subdirectory("tests")
 endif()
 
+if(BUILD_WRAPPERS)
+    add_subdirectory("wrappers")
+endif()
+
 add_custom_target(
     CIIntegration ${CMAKE_COMMAND} -E echo "CI Integration"
     SOURCES appveyor.yml .travis.yml)
diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt
new file mode 100644
index 0000000..2bf5a75
--- /dev/null
+++ b/applications/CMakeLists.txt
@@ -0,0 +1,4 @@
+file(GLOB_RECURSE python_files *.py)
+set(python_files ${python_files} odil)
+add_custom_target(
+    PythonFiles ${CMAKE_COMMAND} -E echo "Python files" SOURCES ${python_files})
diff --git a/applications/echo.py b/applications/echo.py
new file mode 100644
index 0000000..0ed6190
--- /dev/null
+++ b/applications/echo.py
@@ -0,0 +1,38 @@
+import logging
+
+import odil
+
+def add_subparser(subparsers):
+    parser = subparsers.add_parser(
+        "echo", help="Ping a remote DICOM server (C-ECHO)")
+    parser.add_argument("host", help="Remote host address")
+    parser.add_argument("port", type=int, help="Remote host port")
+    parser.add_argument(
+        "calling_ae_title", help="AE title of the calling application")
+    parser.add_argument(
+        "called_ae_title", help="AE title of the called application")
+    parser.set_defaults(function=echo)
+    return parser
+
+def echo(host, port, calling_ae_title, called_ae_title):
+    association = odil.Association()
+    association.set_peer_host(host)
+    association.set_peer_port(port)
+    association.update_parameters()\
+        .set_calling_ae_title(calling_ae_title)\
+        .set_called_ae_title(called_ae_title) \
+        .set_presentation_contexts([
+            odil.AssociationParameters.PresentationContext(
+                3, odil.registry.VerificationSOPClass,
+                [ odil.registry.ImplicitVRLittleEndian ], True, False
+            )
+        ])
+    association.associate()
+    logging.info("Association established")
+
+    echo = odil.EchoSCU(association)
+    echo.echo()
+    logging.info("C-ECHO successful")
+
+    association.release()
+    logging.info("Association released")
diff --git a/applications/find.py b/applications/find.py
new file mode 100644
index 0000000..48710f5
--- /dev/null
+++ b/applications/find.py
@@ -0,0 +1,85 @@
+import logging
+
+import odil
+
+from print_ import find_max_name_length, print_data_set
+
+def add_subparser(subparsers):
+    parser = subparsers.add_parser(
+        "find", help="DICOM query (C-FIND)")
+    parser.add_argument("host", help="Remote host address")
+    parser.add_argument("port", type=int, help="Remote host port")
+    parser.add_argument(
+        "calling_ae_title", help="AE title of the calling application")
+    parser.add_argument(
+        "called_ae_title", help="AE title of the called application")
+    parser.add_argument(
+        "level", choices=["patient", "study"], help="Root object of the query")
+    parser.add_argument("keys", nargs="+", help="Query keys")
+    parser.add_argument(
+        "--decode-uids", "-u", action="store_true",
+        help="Print human-friendly name of known UIDs")
+    parser.set_defaults(function=find)
+    return parser
+
+def find(host, port, calling_ae_title, called_ae_title, level, keys, decode_uids):
+    query = odil.DataSet()
+    for key in keys:
+        if "=" in key:
+            key, value = key.split("=", 1)
+            value = value.split("\\")
+        else:
+            value = None
+        
+        tag = getattr(odil.registry, key)
+        
+        if value is not None:
+            vr = odil.registry.public_dictionary[tag].vr
+            if vr in ["DS", "FL", "FD"]:
+                value = odil.Value.Reals([float(x) for x in value])
+            elif vr in ["IS", "SL", "SS", "UL", "US"]:
+                value = odil.Value.Integers([int(x) for x in value])
+            else:
+                value = odil.Value.Strings(value)
+                
+            query.add(tag, value)
+        else:
+            query.add(tag)
+    
+    sop_class = getattr(
+        odil.registry,
+        "{}RootQueryRetrieveInformationModelFIND".format(level.capitalize()))
+    
+    find_pc = odil.AssociationParameters.PresentationContext(
+        1, sop_class,
+        [
+            odil.registry.ImplicitVRLittleEndian,
+            odil.registry.ExplicitVRLittleEndian
+        ], True, False
+    )
+    
+    association = odil.Association()
+    association.set_peer_host(host)
+    association.set_peer_port(port)
+    association.update_parameters()\
+        .set_calling_ae_title(calling_ae_title)\
+        .set_called_ae_title(called_ae_title) \
+        .set_presentation_contexts([find_pc])
+    association.associate()
+    logging.info("Association established")
+    
+    find = odil.FindSCU(association)
+    find.set_affected_sop_class(sop_class)
+    data_sets = find.find(query)
+    print "{} answer{}".format(len(data_sets), "s" if len(data_sets)>1 else "")
+
+    max_length = 0
+    for data_set in data_sets:
+        max_length = max(max_length, find_max_name_length(data_set))
+
+    for data_set in data_sets:
+        print_data_set(data_set, decode_uids, "", max_length)
+        print
+
+    association.release()
+    logging.info("Association released")
diff --git a/applications/odil b/applications/odil
new file mode 100755
index 0000000..a4ad30d
--- /dev/null
+++ b/applications/odil
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+import argparse
+import logging
+import sys
+
+import echo
+import find
+import print_
+import transcode
+
+def main():
+    parser = argparse.ArgumentParser()
+
+    subparsers = parser.add_subparsers(help="Sub-commands help")
+
+    modules = [
+        print_, transcode, echo, find
+    ]
+    for module in modules:
+        sub_parser = module.add_subparser(subparsers)
+        sub_parser.add_argument(
+            "--verbosity", "-v",
+            choices=["warning", "info", "debug"], default="warning")
+
+    arguments = parser.parse_args()
+
+    verbosity = arguments.__dict__.pop("verbosity")
+    logging.getLogger().setLevel(verbosity.upper())
+
+    function = arguments.__dict__.pop("function")
+    try:
+        function(**arguments.__dict__)
+    except Exception as e:
+        if verbosity == "debug":
+            raise
+        else:
+            parser.error(e)
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/applications/print_.py b/applications/print_.py
new file mode 100644
index 0000000..d612cbc
--- /dev/null
+++ b/applications/print_.py
@@ -0,0 +1,92 @@
+import logging
+
+import odil
+
+def add_subparser(subparsers):
+    parser = subparsers.add_parser(
+        "print", help="Print the contents of data sets")
+    parser.add_argument("inputs", nargs="+", metavar="FILE", help="Input files")
+    parser.add_argument(
+        "--print-header", "-H", action="store_true",
+        help="Print the header as well as the data set")
+    parser.add_argument(
+        "--decode-uids", "-u", action="store_true",
+        help="Print human-friendly name of known UIDs")
+    parser.set_defaults(function=print_)
+    return parser
+
+def print_(inputs, print_header, decode_uids):
+    for input in inputs:
+        logging.info("Printing {}".format(input))
+        header, data_set = odil.read(input)
+
+        max_length = find_max_name_length(data_set)
+        if print_header:
+            max_length = max(max_length, find_max_name_length(header))
+
+        if print_header:
+            print_data_set(header, decode_uids, "", max_length)
+            print
+        print_data_set(data_set, decode_uids, "", max_length)
+
+def print_data_set(data_set, decode_uids, padding, max_length):
+    for tag, element in data_set.items():
+        name = "{:04x},{:04x}".format(tag.group, tag.element)
+        if tag in odil.registry.public_dictionary:
+            entry = odil.registry.public_dictionary[tag]
+            name = entry.name
+
+        if element.is_data_set():
+            value = ""
+        elif element.is_binary():
+            lengths = [len(x) for x in element.as_binary()]
+            value = "(binary, {} item{}, {} byte{})".format(
+                len(element), "s" if len(element)>1 else "",
+                "+".join(str(x) for x in lengths),
+                "s" if sum(lengths)>1 else "")
+        else:
+            getter = None
+            if element.is_int():
+                getter = element.as_int
+            elif element.is_real():
+                getter = element.as_real
+            elif element.is_string():
+                getter = element.as_string
+            value = [x for x in getter()]
+
+            if decode_uids and element.vr == odil.VR.UI:
+                value = [
+                    odil.registry.uids_dictionary[uid].name
+                        if uid in odil.registry.uids_dictionary else uid
+                    for uid in value
+                ]
+
+        print "{}{}{} {:04x},{:04x} {} {}".format(
+            padding,
+            name, (max_length-len(name)-len(padding))*" ",
+            tag.group, tag.element, element.vr,
+            value)
+
+        if element.is_data_set():
+            sequence = element.as_data_set()
+            if sequence:
+                for item in sequence[:-1]:
+                    print_data_set(item, decode_uids, padding+"  ", max_length)
+                    print
+                print_data_set(sequence[-1], decode_uids, padding+"  ", max_length)
+
+def find_max_name_length(data_set, max_length=0, padding_length=0):
+    for tag, element in data_set.items():
+        if tag in odil.registry.public_dictionary:
+            entry = odil.registry.public_dictionary[tag]
+            length = len(entry.name)
+        else:
+            length = 9 # xxxx,yyyy
+        max_length = max(max_length, padding_length+length)
+        if element.is_data_set():
+            sequence = element.as_data_set()
+            for item in sequence:
+                max_length = max(
+                    max_length,
+                    find_max_name_length(item, max_length, 2+padding_length))
+    return max_length
diff --git a/applications/transcode.py b/applications/transcode.py
new file mode 100644
index 0000000..572459e
--- /dev/null
+++ b/applications/transcode.py
@@ -0,0 +1,59 @@
+import logging
+
+import odil
+
+def add_subparser(subparsers):
+    parser = subparsers.add_parser(
+        "transcode",
+        help="Change the transfer syntax or the format")
+    parser.add_argument("input", help="Input file name")
+    parser.add_argument("output", help="Output file name")
+    parser.add_argument(
+        "--format", "-f",
+        choices=["binary", "json", "xml"], default="binary",
+        help="Output file format")
+    parser.add_argument(
+        "--transfer-syntax", "-t",
+        help="Transfer syntax of output file (binary only)")
+    parser.add_argument(
+        "--pretty-print", "-p", action="store_true",
+        help="Pretty-print in output file (JSON and XML only)")
+    parser.set_defaults(function=transcode)
+    return parser
+
+def transcode(input, output, format, transfer_syntax, pretty_print):
+
+    kwargs = {}
+
+    if format == "binary":
+        if transfer_syntax:
+            transfer_syntax = getattr(odil.registry, transfer_syntax)
+            ts_info = odil.registry.uids_dictionary[transfer_syntax]
+            if ts_info.type != "Transfer Syntax":
+                raise Exception(
+                    "{} is not a transfer syntax".format(ts_info.name))
+            kwargs["transfer_syntax"] = transfer_syntax
+        else:
+            kwargs["transfer_syntax"] = None
+    elif format == "json":
+        kwargs["pretty_print"] = pretty_print
+    elif format == "xml":
+        kwargs["pretty_print"] = pretty_print
+
+    globals()["as_{}".format(format)](input, output, **kwargs)
+
+def as_binary(input, output, transfer_syntax):
+    _, data_set = odil.read(input)
+    odil.write(data_set, output, transfer_syntax=transfer_syntax)
+
+def as_json(input, output, pretty_print):
+    _, data_set = odil.read(input)
+    with open(output, "w") as fd:
+        json = odil.as_json(data_set, pretty_print)
+        fd.write(json)
+
+def as_xml(input, output, pretty_print):
+    _, data_set = odil.read(input)
+    with open(output, "w") as fd:
+        xml = odil.as_xml(data_set, pretty_print)
+        fd.write(xml)
diff --git a/appveyor.yml b/appveyor.yml
index f791cea..edbf936 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -25,8 +25,8 @@ install:
 - ps: Start-FileDownload https://github.com/lamyj/jsoncpp/releases/download/0.10.5/jsoncpp_0_10_5_Win64_msvc14.zip
 - 7z x -bd -oC:\Libraries jsoncpp_0_10_5_Win64_msvc14.zip
   # DCMTK
-- ps: Start-FileDownload https://github.com/lamyj/dcmtk/releases/download/DCMTK-3.6.1_20150924/dcmtk-3.6.1_20150924_Win64_msvc14.zip
-- 7z x -bd -oC:\Libraries dcmtk-3.6.1_20150924_Win64_msvc14.zip
+- ps: Start-FileDownload https://github.com/lamyj/dcmtk/releases/download/DCMTK-3.6.1_20150924/dcmtk-3.6.1_20150924_Win64_msvc14_dynamic.zip
+- 7z x -bd -oC:\Libraries dcmtk-3.6.1_20150924_Win64_msvc14_dynamic.zip
 
 before_build:
   - cd c:\projects\odil
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 0c5827e..91d181a 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -1,17 +1,27 @@
+find_package(Boost REQUIRED)
 find_package(DCMTK REQUIRED)
 find_package(JsonCpp REQUIRED)
 
-include_directories(${CMAKE_SOURCE_DIR}/src ${JsonCpp_INCLUDE_DIRS})
+include_directories(
+    ${CMAKE_SOURCE_DIR}/src ${Boost_INCLUDE_DIRS} ${DCMTK_INCLUDE_DIRS}
+    ${JsonCpp_INCLUDE_DIRS})
 add_definitions(
     ${DCMTK_DEFINITIONS}
     -D BOOST_ASIO_DYN_LINK
     -D ODIL_MAJOR_VERSION=${odil_MAJOR_VERSION}
 )
+link_directories(${Boost_LIBRARY_DIRS} ${DCMTK_LIBRARY_DIRS})
 
 file(GLOB_RECURSE examples *.cpp)
 
 foreach(example_file ${examples})
     get_filename_component(example ${example_file} NAME_WE)
     add_executable(${example} ${example_file})
-    target_link_libraries(${example} odil)
+    target_link_libraries(${example} libodil)
 endforeach()
+
+file(GLOB_RECURSE python_examples *.py)
+add_custom_target(
+    PythonExamples
+    ${CMAKE_COMMAND} -E echo "Python examples"
+    SOURCES ${python_examples})
diff --git a/examples/convert.py b/examples/convert.py
new file mode 100644
index 0000000..e392f75
--- /dev/null
+++ b/examples/convert.py
@@ -0,0 +1,48 @@
+import argparse
+import sys
+
+import odil
+
+def main():
+    parser = argparse.ArgumentParser(description="Convert a DICOM file")
+    parser.add_argument("input")
+    parser.add_argument("output")
+    parser.add_argument("--transfer-syntax", "-t", help="Transfer syntax")
+    parser.add_argument(
+        "--item-length", "-i", choices=["Explicit", "Undefined"], 
+        default="Explicit", help="Item length encoding")
+    parser.add_argument(
+        "--use-group-length", "-g", action="store_true", 
+        help="Include group length tags")
+    arguments = parser.parse_args()
+
+    if arguments.transfer_syntax not in dir(odil.registry):
+        parser.error("Unknown transfer syntax")
+    uid = getattr(odil.registry, arguments.transfer_syntax)
+    if uid not in odil.registry.uids_dictionary:
+        parser.error("Unknown transfer syntax")
+    if odil.registry.uids_dictionary[uid].type != "Transfer Syntax":
+        parser.error("Not a transfer syntax")
+    arguments.transfer_syntax = uid
+
+    arguments.item_length = getattr(
+        odil.Writer.ItemEncoding, "{}Length".format(arguments.item_length))
+
+    convert(**arguments.__dict__)
+
+def convert(input, output, transfer_syntax, item_length, use_group_length):
+    header, data_set = odil.read(input)
+
+    to_remove = [
+        "FileMetaInformationVersion",
+        "MediaStorageSOPClassUID", "MediaStorageSOPInstanceUID", 
+        "TransferSyntaxUID", 
+        "ImplementationClassUID", "ImplementationVersionName"
+    ]
+    for name in to_remove:
+        header.remove(getattr(odil.registry, name))
+
+    odil.write(data_set, output, header, transfer_syntax, item_length, use_group_length)
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/examples/dump.py b/examples/dump.py
new file mode 100644
index 0000000..211e656
--- /dev/null
+++ b/examples/dump.py
@@ -0,0 +1,69 @@
+import argparse
+import sys
+
+import odil
+
+def main():
+    parser = argparse.ArgumentParser(description="Print content of DICOM file")
+    parser.add_argument("file")
+    parser.add_argument(
+        "--header", "-H", action="store_true", help="Print header")
+    parser.add_argument(
+        "--decode-uids", "-D", action="store_true", help="Display UID names")
+    arguments = parser.parse_args()
+
+    header, data_set = odil.read(arguments.file)
+    if arguments.header:
+        print_data_set(header, arguments.decode_uids)
+        print
+    print_data_set(data_set, arguments.decode_uids)
+
+def print_data_set(data_set, decode_uids=False, padding=""):
+    max_length = 0
+    for tag in data_set.keys():
+        if tag in odil.registry.public_dictionary:
+            entry = odil.registry.public_dictionary[tag]
+            max_length = max(max_length, len(entry.name))
+        
+    for tag, element in data_set.items():
+        name = "{:04x},{:04x}".format(tag.group, tag.element)
+        if tag in odil.registry.public_dictionary:
+            entry = odil.registry.public_dictionary[tag]
+            name = entry.name
+
+        if element.is_data_set():
+            value = ""
+        elif element.is_binary():
+            length = len(element.as_binary())
+            value = "(binary, {} byte{})".format(length, "s" if length>1 else "")
+        else:
+            getter = None
+            if element.is_int():
+                getter = element.as_int
+            elif element.is_real():
+                getter = element.as_real
+            elif element.is_string():
+                getter = element.as_string
+            value = [x for x in getter()]
+
+            if decode_uids and element.vr == odil.VR.UI:
+                value = [
+                    odil.registry.uids_dictionary[uid].name
+                        if uid in odil.registry.uids_dictionary else uid
+                    for uid in value
+                ]
+
+        print "{}{}{} {:04x},{:04x} {} {}".format(
+            padding,
+            name, (max_length-len(name))*" ", 
+            tag.group, tag.element, element.vr,
+            value)
+
+        if element.is_data_set():
+            for item in element.as_data_set()[:-1]:
+                print_data_set(item, decode_uids, padding+"  ")
+                print
+            print_data_set(element.as_data_set()[-1], padding+"  ")
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/examples/find.cpp b/examples/find.cpp
index 4c5b1e0..7540efc 100644
--- a/examples/find.cpp
+++ b/examples/find.cpp
@@ -2,6 +2,7 @@
 
 #include "odil/Association.h"
 #include "odil/DataSet.h"
+#include "odil/EchoSCU.h"
 #include "odil/FindSCU.h"
 #include "odil/registry.h"
 
@@ -37,11 +38,12 @@ int main()
         });
     
     association.associate();
+
+    odil::EchoSCU echo_scu(association);
+    echo_scu.echo();
     
     odil::FindSCU scu(association);
 
-    scu.echo();
-
     odil::DataSet query;
     query.add("PatientName", { "*" });
     query.add("QueryRetrieveLevel", { "STUDY" });
diff --git a/examples/find.py b/examples/find.py
new file mode 100644
index 0000000..56e3023
--- /dev/null
+++ b/examples/find.py
@@ -0,0 +1,40 @@
+import odil
+
+association = odil.Association()
+association.set_peer_host("184.73.255.26")
+association.set_peer_port(11112)
+
+presentation_contexts = [
+    odil.AssociationParameters.PresentationContext(
+        1, odil.registry.StudyRootQueryRetrieveInformationModelFIND,
+        [ odil.registry.ExplicitVRLittleEndian ], True, False
+    ),
+    odil.AssociationParameters.PresentationContext(
+        3, odil.registry.VerificationSOPClass,
+        [ odil.registry.ExplicitVRLittleEndian ], True, False
+    )
+]
+
+association.update_parameters()\
+    .set_calling_ae_title("myself")\
+    .set_called_ae_title("AWSPIXELMEDPUB") \
+    .set_presentation_contexts(presentation_contexts)
+association.associate()
+
+query = odil.DataSet()
+query.add(odil.registry.PatientName, odil.Value.Strings(["*"]))
+query.add(odil.registry.QueryRetrieveLevel, odil.Value.Strings(["STUDY"]))
+query.add(odil.registry.StudyDescription)
+query.add(odil.registry.StudyDate)
+
+find = odil.FindSCU(association)
+find.set_affected_sop_class(odil.registry.StudyRootQueryRetrieveInformationModelFIND)
+
+def callback(data_set):
+    print data_set.as_string(odil.registry.PatientName)[0]
+find.find(query, callback)
+
+data_sets = find.find(query)
+print len(data_sets), "found"
+for data_set in data_sets:
+    print data_set.as_string(odil.registry.PatientName)[0]
diff --git a/examples/store.cpp b/examples/store.cpp
index 4e3d170..9704993 100644
--- a/examples/store.cpp
+++ b/examples/store.cpp
@@ -1,5 +1,6 @@
 #include "odil/Association.h"
 #include "odil/DataSet.h"
+#include "odil/EchoSCU.h"
 #include "odil/Reader.h"
 #include "odil/StoreSCU.h"
 
@@ -23,11 +24,12 @@ int main(int argc, char** argv)
         });
     
     association.associate();
+
+    odil::EchoSCU echo_scu(association);
+    echo_scu.echo();
     
     odil::StoreSCU scu(association);
     
-    scu.echo();
-    
     for(int i=1; i<argc; ++i)
     {
         std::ifstream stream(argv[i], std::ios::binary);
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0d582eb..5218b46 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -17,11 +17,12 @@ add_definitions(
 )
 link_directories(${Boost_LIBRARY_DIRS} ${DCMTK_LIBRARY_DIRS})
 
-add_library(odil SHARED ${files} ${headers} ${templates})
+add_library(libodil SHARED ${files} ${headers} ${templates})
+set_target_properties(libodil PROPERTIES OUTPUT_NAME odil)
 
-target_link_libraries(odil
+target_link_libraries(libodil
     ${Boost_LIBRARIES} ${DCMTK_LIBRARIES} ${ICU_LIBRARIES} ${JsonCpp_LIBRARIES})
-set_target_properties(odil PROPERTIES
+set_target_properties(libodil PROPERTIES
     VERSION ${odil_VERSION}
     SOVERSION ${odil_MAJOR_VERSION})
 
@@ -29,7 +30,7 @@ install(
     DIRECTORY odil/ DESTINATION include/odil
     FILES_MATCHING PATTERN "*.h" PATTERN "*.txx")
 install(
-    TARGETS odil
+    TARGETS libodil
     ARCHIVE DESTINATION lib
     LIBRARY DESTINATION lib
     RUNTIME DESTINATION bin)
diff --git a/src/odil/AssociationParameters.cpp b/src/odil/AssociationParameters.cpp
index ccae6ed..7017b5b 100644
--- a/src/odil/AssociationParameters.cpp
+++ b/src/odil/AssociationParameters.cpp
@@ -27,6 +27,49 @@
 namespace odil
 {
 
+bool 
+AssociationParameters::PresentationContext
+::operator==(PresentationContext const & other) const
+{
+    return (
+        this->id == other.id && 
+        this->abstract_syntax == other.abstract_syntax &&
+        this->transfer_syntaxes == other.transfer_syntaxes &&
+        this->scu_role_support == other.scu_role_support &&
+        this->scp_role_support == other.scp_role_support &&
+        this->result == other.result
+    );
+}
+
+bool 
+AssociationParameters::UserIdentity
+::operator==(UserIdentity const & other) const
+{
+    return (
+        this->type == other.type &&
+        (
+            this->type == Type::None ||
+            (
+                this->type == Type::Username &&
+                this->primary_field == other.primary_field
+            ) ||
+            (
+                this->type == Type::UsernameAndPassword &&
+                this->primary_field == other.primary_field &&
+                this->secondary_field == other.secondary_field
+            ) ||
+            (
+                this->type == Type::Kerberos &&
+                this->primary_field == other.primary_field
+            ) ||
+            (
+                this->type == Type::SAML &&
+                this->primary_field == other.primary_field
+            ) 
+        )
+    );
+}
+
 AssociationParameters
 ::AssociationParameters()
 : _called_ae_title(""), _calling_ae_title(""), _presentation_contexts(),
@@ -446,6 +489,19 @@ AssociationParameters
     return pdu;
 }
 
+bool 
+AssociationParameters
+::operator==(AssociationParameters const & other) const
+{
+    return (
+        this->get_called_ae_title() == other.get_called_ae_title() &&
+        this->get_calling_ae_title() == other.get_calling_ae_title() &&
+        this->get_presentation_contexts() == other.get_presentation_contexts() &&
+        this->get_user_identity() == other.get_user_identity() &&
+        this->get_maximum_length() == other.get_maximum_length()
+    );
+}
+
 AssociationParameters &
 AssociationParameters
 ::_set_user_identity(UserIdentity const & value)
diff --git a/src/odil/AssociationParameters.h b/src/odil/AssociationParameters.h
index c251834..aa9c37b 100644
--- a/src/odil/AssociationParameters.h
+++ b/src/odil/AssociationParameters.h
@@ -44,6 +44,9 @@ public:
         bool scu_role_support;
         bool scp_role_support;
         Result result;
+
+        /// @brief Member-wise equality.
+        bool operator==(PresentationContext const & other) const;
     };
 
     /// @brief User Identity, cf. PS3.8 D.3.3.7
@@ -61,6 +64,9 @@ public:
         Type type;
         std::string primary_field;
         std::string secondary_field;
+
+        /// @brief Member-wise equality.
+        bool operator==(UserIdentity const & other) const;
     };
 
     /// @brief Constructor.
@@ -140,6 +146,9 @@ public:
     /// @brief Create an A-ASSOCIATE-AC PDU.
     pdu::AAssociateAC as_a_associate_ac() const;
 
+    /// @brief Member-wise equality.
+    bool operator==(AssociationParameters const & other) const;
+
 private:
     std::string _called_ae_title;
     std::string _calling_ae_title;
diff --git a/src/odil/BasicDirectoryCreator.cpp b/src/odil/BasicDirectoryCreator.cpp
index b3c5aa8..d73aa89 100644
--- a/src/odil/BasicDirectoryCreator.cpp
+++ b/src/odil/BasicDirectoryCreator.cpp
@@ -357,7 +357,7 @@ BasicDirectoryCreator
 {
     DataSet meta_information;
     meta_information.add(
-        registry::FileMetaInformationVersion, Value::Binary({0x00, 0x01}));
+        registry::FileMetaInformationVersion, Value::Binary({{0x00, 0x01}}));
     meta_information.add(
         registry::MediaStorageSOPClassUID,
         { registry::MediaStorageDirectoryStorage });
diff --git a/src/odil/SCU.cpp b/src/odil/EchoSCU.cpp
similarity index 77%
copy from src/odil/SCU.cpp
copy to src/odil/EchoSCU.cpp
index 0952b9d..d323d42 100644
--- a/src/odil/SCU.cpp
+++ b/src/odil/EchoSCU.cpp
@@ -6,49 +6,32 @@
  * for details.
  ************************************************************************/
 
-#include "odil/SCU.h"
-
-#include <string>
+#include "odil/EchoSCU.h"
 
 #include "odil/Association.h"
 #include "odil/Exception.h"
 #include "odil/message/CEchoRequest.h"
 #include "odil/message/CEchoResponse.h"
-#include "odil/message/Message.h"
 #include "odil/registry.h"
 
 namespace odil
 {
 
-SCU
-::SCU(Association & association)
-: _association(association), _affected_sop_class("")
+EchoSCU
+::EchoSCU(Association & association)
+: SCU(association)
 {
     // Nothing else.
 }
 
-SCU
-::~SCU()
+EchoSCU
+::~EchoSCU()
 {
     // Nothing to do.
 }
 
-std::string const &
-SCU
-::get_affected_sop_class() const
-{
-    return this->_affected_sop_class;
-}
-    
-void 
-SCU
-::set_affected_sop_class(std::string const & sop_class)
-{
-    this->_affected_sop_class = sop_class;
-}
-
 void
-SCU
+EchoSCU
 ::echo() const
 {
     auto const message_id = this->_association.next_message_id();
@@ -70,3 +53,4 @@ SCU
 }
 
 }
+
diff --git a/src/odil/EchoSCU.h b/src/odil/EchoSCU.h
new file mode 100644
index 0000000..5ad96d8
--- /dev/null
+++ b/src/odil/EchoSCU.h
@@ -0,0 +1,35 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#ifndef _94f3b347_1f95_49ab_83f6_8710f5a3ad67
+#define _94f3b347_1f95_49ab_83f6_8710f5a3ad67
+
+#include "odil/Association.h"
+#include "odil/SCU.h"
+
+namespace odil
+{
+
+/// @brief SCU for the C-ECHO services.
+class EchoSCU: public SCU
+{
+public:
+    /// @brief Constructor
+    EchoSCU(Association & association);
+
+    /// @brief Destructor.
+    virtual ~EchoSCU();
+    
+    /// @brief Perform DICOM ping
+    void echo() const;
+};
+
+}
+
+#endif // _94f3b347_1f95_49ab_83f6_8710f5a3ad67
+
diff --git a/src/odil/ElementsDictionary.cpp b/src/odil/ElementsDictionary.cpp
index 1612bf1..5fc6363 100644
--- a/src/odil/ElementsDictionary.cpp
+++ b/src/odil/ElementsDictionary.cpp
@@ -103,6 +103,30 @@ ElementsDictionaryKey
         return static_cast<int>(this->_type) < static_cast<int>(other._type);
     }
 }
+bool
+ElementsDictionaryKey
+::operator==(ElementsDictionaryKey const & other) const
+{
+    if(this->_type == other._type)
+    {
+        if(this->_type == Type::Tag)
+        {
+            return (this->_tag == other._tag);
+        }
+        else if(this->_type == Type::String)
+        {
+            return (this->_string == other._string);
+        }
+        else
+        {
+            throw Exception("Invalid type");
+        }
+    }
+    else
+    {
+        return false;
+    }
+}
 
 ElementsDictionaryEntry
 ::ElementsDictionaryEntry(
diff --git a/src/odil/ElementsDictionary.h b/src/odil/ElementsDictionary.h
index 7985da6..2443c5e 100644
--- a/src/odil/ElementsDictionary.h
+++ b/src/odil/ElementsDictionary.h
@@ -56,6 +56,9 @@ namespace odil
         /// @brief Comparator.
         bool operator<(ElementsDictionaryKey const & other) const;
 
+        /// @brief Comparator.
+        bool operator==(ElementsDictionaryKey const & other) const;
+
     private:
         Type _type;
         Tag _tag;
@@ -73,8 +76,8 @@ struct ElementsDictionaryEntry
     std::string vm;
 
     ElementsDictionaryEntry(
-        std::string const & name, std::string const & keyword,
-        std::string const & vr, std::string const & vm);
+        std::string const & name="", std::string const & keyword="",
+        std::string const & vr="", std::string const & vm="");
 };
 
 typedef
diff --git a/src/odil/Reader.cpp b/src/odil/Reader.cpp
index 9e69061..5964c41 100644
--- a/src/odil/Reader.cpp
+++ b/src/odil/Reader.cpp
@@ -319,7 +319,11 @@ Reader::Visitor
             value.resize(strings.size());
             std::transform(
                 strings.begin(), strings.end(), value.begin(),
-                [](std::string const & s) { return std::stod(s); });
+                [](std::string const & s)
+                {
+                    return static_cast<Value::Real>(std::stold(s));
+                }
+            );
         }
     }
     else
@@ -465,51 +469,54 @@ Reader::Visitor
 ::operator()(Value::Binary & value) const
 {
     auto const vl = this->read_length();
-
-    if(this->vr == VR::OB)
+    if(vl == 0xffffffff)
     {
-        value.resize(vl);
-        this->stream.read(reinterpret_cast<char*>(&value[0]), value.size());
+        value = this->read_encapsulated_pixel_data(this->stream);
     }
-    else if(this->vr == VR::OF)
+    else
     {
-        if(vl%4 != 0)
+        value.resize(1);
+        if(this->vr == VR::OB || this->vr == VR::UN)
         {
-            throw Exception("Cannot read OF for odd-sized array");
+            value[0].resize(vl);
+            this->stream.read(
+                reinterpret_cast<char*>(&value[0][0]), value[0].size());
         }
-
-        value.resize(vl);
-        for(unsigned int i=0; i<value.size(); i+=4)
+        else if(this->vr == VR::OF)
         {
-            odil_read_binary(
-                float, item, this->stream, this->byte_ordering, 32);
-            *reinterpret_cast<float*>(&value[i]) = item;
+            if(vl%4 != 0)
+            {
+                throw Exception("Cannot read OF for odd-sized array");
+            }
+
+            value[0].resize(vl);
+            for(unsigned int i=0; i<value[0].size(); i+=4)
+            {
+                odil_read_binary(
+                    float, item, this->stream, this->byte_ordering, 32);
+                *reinterpret_cast<float*>(&value[0][i]) = item;
+            }
         }
-    }
-    else if(this->vr == VR::OW)
-    {
-        if(vl%2 != 0)
+        else if(this->vr == VR::OW)
         {
-            throw Exception("Cannot read OW for odd-sized array");
-        }
+            if(vl%2 != 0)
+            {
+                throw Exception("Cannot read OW for odd-sized array");
+            }
 
-        value.resize(vl);
-        for(unsigned int i=0; i<value.size(); i+=2)
+            value[0].resize(vl);
+            for(unsigned int i=0; i<value[0].size(); i+=2)
+            {
+                odil_read_binary(
+                    uint16_t, item, this->stream, this->byte_ordering, 16);
+                *reinterpret_cast<uint16_t*>(&value[0][i]) = item;
+            }
+        }
+        else
         {
-            odil_read_binary(
-                uint16_t, item, this->stream, this->byte_ordering, 16);
-            *reinterpret_cast<uint16_t*>(&value[i]) = item;
+            throw Exception("Cannot read "+as_string(this->vr)+" as binary");
         }
     }
-    else if(this->vr == VR::UN)
-    {
-        value.resize(vl);
-        this->stream.read(reinterpret_cast<char*>(&value[0]), value.size());
-    }
-    else
-    {
-        throw Exception("Cannot read "+as_string(this->vr)+" as binary");
-    }
 }
 
 uint32_t
@@ -605,6 +612,53 @@ Reader::Visitor
     return item;
 }
 
+Value::Binary
+Reader::Visitor
+::read_encapsulated_pixel_data(std::istream & specific_stream) const
+{
+    Value::Binary value;
+
+    // PS 3.5, A.4
+    Reader const sequence_reader(
+        specific_stream, this->transfer_syntax, this->keep_group_length);
+    bool done = false;
+    while(!done)
+    {
+        auto const tag = sequence_reader.read_tag();
+        odil_read_binary(
+            uint32_t, item_length, specific_stream, this->byte_ordering, 32);
+
+        if(tag == registry::Item)
+        {
+            Value::Binary::value_type item_data(item_length);
+
+            if(item_length > 0)
+            {
+                specific_stream.read(
+                    reinterpret_cast<char*>(&item_data[0]), item_length);
+                if(!stream)
+                {
+                    throw Exception("Could not read from stream");
+                }
+            }
+
+            value.push_back(item_data);
+        }
+        else if(tag == registry::SequenceDelimitationItem)
+        {
+            // No value for Sequence Delimitation Item
+            done = true;
+        }
+        else
+        {
+            throw Exception(
+                "Expected SequenceDelimitationItem, got: "+std::string(tag));
+        }
+    }
+
+    return value;
+}
+
 }
 
 #undef odil_ignore
diff --git a/src/odil/Reader.h b/src/odil/Reader.h
index aa3528b..14588a5 100644
--- a/src/odil/Reader.h
+++ b/src/odil/Reader.h
@@ -98,6 +98,8 @@ private:
 
         Value::Strings split_strings(std::string const & string) const;
         DataSet read_item(std::istream & specific_stream) const;
+        Value::Binary read_encapsulated_pixel_data(
+            std::istream & specific_stream) const;
     };
 };
 
diff --git a/src/odil/SCU.cpp b/src/odil/SCU.cpp
index 0952b9d..5f90d07 100644
--- a/src/odil/SCU.cpp
+++ b/src/odil/SCU.cpp
@@ -47,26 +47,4 @@ SCU
     this->_affected_sop_class = sop_class;
 }
 
-void
-SCU
-::echo() const
-{
-    auto const message_id = this->_association.next_message_id();
-    
-    message::CEchoRequest const request(
-        message_id, registry::VerificationSOPClass);
-    this->_association.send_message(
-        request, request.get_affected_sop_class_uid());
-    
-    message::CEchoResponse const response = this->_association.receive_message();
-    if(response.get_message_id_being_responded_to() != message_id)
-    {
-        std::ostringstream message;
-        message << "DIMSE: Unexpected Response MsgId: "
-                << response.get_message_id_being_responded_to() 
-                << "(expected: " << message_id << ")";
-        throw Exception(message.str());
-    }
-}
-
 }
diff --git a/src/odil/SCU.h b/src/odil/SCU.h
index 1c51249..d6696b8 100644
--- a/src/odil/SCU.h
+++ b/src/odil/SCU.h
@@ -31,9 +31,6 @@ public:
     /// @brief Set the affected SOP class
     void set_affected_sop_class(std::string const & sop_class);
     
-    /// @brief Perform DICOM ping
-    void echo() const;
-
 protected:
     Association & _association;
     /// @brief Affected SOP class.
diff --git a/src/odil/UIDsDictionary.h b/src/odil/UIDsDictionary.h
index a82d131..fa36a14 100644
--- a/src/odil/UIDsDictionary.h
+++ b/src/odil/UIDsDictionary.h
@@ -25,8 +25,8 @@ struct UIDsDictionaryEntry
     std::string type;
 
     UIDsDictionaryEntry(
-        std::string const & name, std::string const & keyword,
-        std::string const & type);
+        std::string const & name="", std::string const & keyword="",
+        std::string const & type="");
 };
 
 typedef std::map<std::string, UIDsDictionaryEntry> UIDsDictionary;
diff --git a/src/odil/VR.cpp b/src/odil/VR.cpp
index 73c5fef..4590417 100644
--- a/src/odil/VR.cpp
+++ b/src/odil/VR.cpp
@@ -131,7 +131,7 @@ bool is_real(VR vr)
 bool is_string(VR vr)
 {
     return (
-        vr == VR::AE || vr == VR::AS || vr == VR::CS || vr == VR::DA ||
+        vr == VR::AE || vr == VR::AS || vr == VR::AT || vr == VR::CS || vr == VR::DA ||
         vr == VR::DT || vr == VR::LO || vr == VR::LT || vr == VR::PN ||
         vr == VR::SH || vr == VR::ST || vr == VR::TM || vr == VR::UC ||
         vr == VR::UI || vr == VR::UR || vr == VR::UT);
diff --git a/src/odil/Value.h b/src/odil/Value.h
index 231ad74..522a577 100644
--- a/src/odil/Value.h
+++ b/src/odil/Value.h
@@ -55,7 +55,7 @@ public:
     typedef std::vector<DataSet> DataSets;
 
     /// @brief Binary data container.
-    typedef std::vector<uint8_t> Binary;
+    typedef std::vector<std::vector<uint8_t>> Binary;
 
     /// @brief Build an empty value.
     Value();
diff --git a/src/odil/Writer.cpp b/src/odil/Writer.cpp
index 4998a20..c229ed1 100644
--- a/src/odil/Writer.cpp
+++ b/src/odil/Writer.cpp
@@ -173,6 +173,10 @@ Writer
             {
                 vl = 0xffffffff;
             }
+            else if((vr == VR::OB || vr == VR::OW) && element.size() > 1)
+            {
+                vl = 0xffffffff;
+            }
             else
             {
                vl = value_stream.tellp();
@@ -206,7 +210,7 @@ Writer
     // Build File Meta Information, PS3.10, 7.1
     DataSet meta_info = meta_information;
     meta_info.add(
-        registry::FileMetaInformationVersion, Value::Binary({0x00, 0x01}));
+        registry::FileMetaInformationVersion, Value::Binary({{0x00, 0x01}}));
 
     if(!data_set.has(registry::SOPClassUID))
     {
@@ -459,47 +463,55 @@ Writer::Visitor::result_type
 Writer::Visitor
 ::operator()(Value::Binary const & value) const
 {
-    if(this->vr == VR::OB || this->vr == VR::UN)
+    if(value.size() > 1)
     {
-        this->stream.write(reinterpret_cast<char const*>(&value[0]), value.size());
+        this->write_encapsulated_pixel_data(value);
     }
-    else if(this->vr == VR::OW)
+    else
     {
-        if(value.size()%2 != 0)
+        if(this->vr == VR::OB || this->vr == VR::UN)
         {
-            throw Exception("Value cannot be written as OW");
+            this->stream.write(
+                reinterpret_cast<char const*>(&value[0][0]), value[0].size());
         }
-        for(int i=0; i<value.size(); i+=2)
+        else if(this->vr == VR::OW)
         {
-            uint16_t item = *reinterpret_cast<uint16_t const *>(&value[i]);
-            odil_write_binary(item, this->stream, this->byte_ordering, 16);
+            if(value[0].size()%2 != 0)
+            {
+                throw Exception("Value cannot be written as OW");
+            }
+            for(int i=0; i<value[0].size(); i+=2)
+            {
+                uint16_t item = *reinterpret_cast<uint16_t const *>(&value[0][i]);
+                odil_write_binary(item, this->stream, this->byte_ordering, 16);
+            }
         }
-    }
-    else if(this->vr == VR::OF)
-    {
-        if(value.size()%4 != 0)
+        else if(this->vr == VR::OF)
         {
-            throw Exception("Value cannot be written as OF");
+            if(value[0].size()%4 != 0)
+            {
+                throw Exception("Value cannot be written as OF");
+            }
+            for(int i=0; i<value[0].size(); i+=4)
+            {
+                uint32_t item = *reinterpret_cast<uint32_t const *>(&value[0][i]);
+                odil_write_binary(item, this->stream, this->byte_ordering, 32);
+            }
         }
-        for(int i=0; i<value.size(); i+=4)
+        else
         {
-            uint32_t item = *reinterpret_cast<uint32_t const *>(&value[i]);
-            odil_write_binary(item, this->stream, this->byte_ordering, 32);
+            throw Exception("Cannot write "+as_string(this->vr)+" as binary");
         }
-    }
-    else
-    {
-        throw Exception("Cannot write "+as_string(this->vr)+" as binary");
-    }
 
-    if(!this->stream)
-    {
-        throw Exception("Could not write to stream");
-    }
+        if(!this->stream)
+        {
+            throw Exception("Could not write to stream");
+        }
 
-    if(value.size()%2 == 1)
-    {
-        this->stream.put('\0');
+        if(value[0].size()%2 == 1)
+        {
+            this->stream.put('\0');
+        }
     }
 }
 
@@ -541,6 +553,35 @@ Writer::Visitor
     }
 }
 
+void
+Writer::Visitor
+::write_encapsulated_pixel_data(Value::Binary const & value) const
+{
+    // FIXME: handle fragments
+    Writer writer(this->stream, this->byte_ordering, this->explicit_vr);
+    uint32_t length;
+    for(auto const & fragment: value)
+    {
+        writer.write_tag(registry::Item);
+        length = fragment.size();
+        odil_write_binary(
+            length, this->stream, this->byte_ordering, 8*sizeof(length));
+        this->stream.write(reinterpret_cast<char const*>(&fragment[0]), length);
+        if(!this->stream)
+        {
+            throw Exception("Could not write to stream");
+        }
+    }
+    writer.write_tag(registry::SequenceDelimitationItem);
+    length = 0;
+    odil_write_binary(
+        length, this->stream, this->byte_ordering, 8*sizeof(length));
+    if(!this->stream)
+    {
+        throw Exception("Could not write to stream");
+    }
+}
+
 }
 
 #undef odil_write_binary
diff --git a/src/odil/Writer.h b/src/odil/Writer.h
index 81a3c1e..4998307 100644
--- a/src/odil/Writer.h
+++ b/src/odil/Writer.h
@@ -107,6 +107,8 @@ private:
 
         template<typename T>
         void write_strings(T const & sequence, char padding) const;
+
+        void write_encapsulated_pixel_data(Value::Binary const & value) const;
     };
 };
 
diff --git a/src/odil/dcmtk/conversion.cpp b/src/odil/dcmtk/conversion.cpp
index ebb57a5..d4ce23c 100644
--- a/src/odil/dcmtk/conversion.cpp
+++ b/src/odil/dcmtk/conversion.cpp
@@ -332,8 +332,10 @@ void convert<std::vector<uint8_t>, Value::Binary>(
     DcmElement * source, Element & destination,
     Value::Binary & (Element::*getter)())
 {
+    // FIXME: does DCMTK only handle a single fragment?
     auto & destination_values = (destination.*getter)();
-    destination_values =
+    destination_values.resize(1);
+    destination_values[0] =
         ElementAccessor<std::vector<uint8_t>>::element_get(*source, 0);
 }
 
@@ -435,17 +437,21 @@ Element convert(DcmElement * source)
 void convert(Element const & source, DcmOtherByteOtherWord * destination)
 {
     auto const & value = source.as_binary();
+    if(value.size() > 1)
+    {
+        throw Exception("Cannot convert multiple valued binary element");
+    }
 
     Uint8 * output;
     OFCondition condition;
     if(destination->getTag().getVR().getValidEVR() == EVR_OB)
     {
-        condition = destination->createUint8Array(source.size(), output);
+        condition = destination->createUint8Array(value[0].size(), output);
     }
     else
     {
         Uint16* temp;
-        condition = destination->createUint16Array(source.size()/2, temp);
+        condition = destination->createUint16Array(value[0].size()/2, temp);
         output = reinterpret_cast<Uint8*>(temp);
     }
 
@@ -454,21 +460,25 @@ void convert(Element const & source, DcmOtherByteOtherWord * destination)
         throw Exception(condition);
     }
 
-    std::copy(value.begin(), value.end(), output);
+    std::copy(value[0].begin(), value[0].end(), output);
 }
 
 void convert(Element const & source, DcmOtherFloat * destination)
 {
     auto const & value = source.as_binary();
+    if(value.size() > 1)
+    {
+        throw Exception("Cannot convert multiple valued binary element");
+    }
 
-    if(value.size()%4 != 0)
+    if(value[0].size()%4 != 0)
     {
         throw Exception("Cannot convert OF from odd-sized array");
     }
 
-    for(unsigned int i=0; i<value.size()/4; ++i)
+    for(unsigned int i=0; i<value[0].size()/4; ++i)
     {
-        float const f = *reinterpret_cast<float const *>(&value[i*4]);
+        float const f = *reinterpret_cast<float const *>(&value[0][i*4]);
         destination->putFloat32(f, i);
     }
 }
diff --git a/src/odil/json_converter.cpp b/src/odil/json_converter.cpp
index 3b5e5ef..70ef96a 100644
--- a/src/odil/json_converter.cpp
+++ b/src/odil/json_converter.cpp
@@ -137,13 +137,22 @@ struct ToJSONVisitor
 
     result_type operator()(VR const vr, Value::Binary const & value) const
     {
+        if(value.size() > 1)
+        {
+            // PS3.18 2016b, F.2.7: There is a single InlineBinary value
+            // representing the entire Value Field.
+            // PS3.18 2016b, Figure 6.5-1: Pixel data is not encoded in
+            // JSON/XML, but transfered using a different content type
+            throw Exception("Binary element is multiple-valued");
+        }
+
         result_type result;
 
         result["vr"] = as_string(vr);
 
         std::string encoded;
-        encoded.reserve(value.size()*4/3);
-        base64::encode(value.begin(), value.end(), std::back_inserter(encoded));
+        encoded.reserve(value[0].size()*4/3);
+        base64::encode(value[0].begin(), value[0].end(), std::back_inserter(encoded));
         result["InlineBinary"] = encoded;
 
         return result;
@@ -254,11 +263,14 @@ DataSet as_dataset(Json::Value const & json)
         {
             element = Element(Value::Binary(), vr);
 
+            // cf. ToJSONVisitor::operator()(VR, Value::Binary): InlineBinary is
+            // single-valued
             auto const & encoded = json_element["InlineBinary"].asString();
             auto & decoded = element.as_binary();
+            decoded.resize(1);
             decoded.reserve(encoded.size()*3/4);
             base64::decode(
-                encoded.begin(), encoded.end(), std::back_inserter(decoded));
+                encoded.begin(), encoded.end(), std::back_inserter(decoded[0]));
         }
         else
         {
diff --git a/src/odil/xml_converter.cpp b/src/odil/xml_converter.cpp
index 227f2f5..187497d 100644
--- a/src/odil/xml_converter.cpp
+++ b/src/odil/xml_converter.cpp
@@ -208,6 +208,15 @@ struct ToXMLVisitor
 
     result_type operator()(VR const vr, Value::Binary const & value) const
     {
+        if(value.size() > 1)
+        {
+            // PS3.18 2016b, F.2.7: There is a single InlineBinary value
+            // representing the entire Value Field.
+            // PS3.18 2016b, Figure 6.5-1: Pixel data is not encoded in
+            // JSON/XML, but transfered using a different content type
+            throw Exception("Binary element is multiple-valued");
+        }
+
         result_type result;
 
         result.put("<xmlattr>.vr", as_string(vr)); // Mandatory
@@ -215,8 +224,8 @@ struct ToXMLVisitor
         boost::property_tree::ptree tag_value;
 
         std::string encoded;
-        encoded.reserve(value.size()*4/3);
-        base64::encode(value.begin(), value.end(), std::back_inserter(encoded));
+        encoded.reserve(value[0].size()*4/3);
+        base64::encode(value[0].begin(), value[0].end(), std::back_inserter(encoded));
         tag_value.put_value(encoded);
 
         result.add_child("InlineBinary", tag_value);
@@ -328,16 +337,15 @@ boost::property_tree::ptree as_xml(DataSet const & data_set)
         boost::property_tree::ptree dicomattribute =
                 apply_visitor(ToXMLVisitor(), element);
 
-        auto const dictionary_it = registry::public_dictionary.find(tag);
-        if(dictionary_it == registry::public_dictionary.end())
-        {
-            throw Exception("No such element: " + std::string(tag));
-        }
-
         // Add Mandatory attribute Tag
         dicomattribute.put("<xmlattr>.tag",  std::string(tag));
         // Add Optional attribute Keyword
-        dicomattribute.put("<xmlattr>.keyword", dictionary_it->second.keyword);
+        auto const dictionary_it = registry::public_dictionary.find(tag);
+        if(dictionary_it != registry::public_dictionary.end())
+        {
+            dicomattribute.put(
+                "<xmlattr>.keyword", dictionary_it->second.keyword);
+        }
         // Add Optional attribute PrivateCreator
         //dicomattribute.put("<xmlattr>.privateCreator", todo);
 
@@ -566,11 +574,14 @@ DataSet as_dataset(boost::property_tree::ptree const & xml)
                 }
 
                 auto const & encoded = it_value->second.get_value<std::string>();
+                // cf. ToXMLVisitor::operator()(VR, Value::Binary): InlineBinary
+                // is single-valued
                 auto & decoded = element.as_binary();
-                decoded.reserve(encoded.size()*3/4);
+                decoded.resize(1);
+                decoded[0].reserve(encoded.size()*3/4);
                 base64::decode(
                     encoded.begin(), encoded.end(),
-                    std::back_inserter(decoded));
+                    std::back_inserter(decoded[0]));
 
                 find_inline_binary = true;
             }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index d112b72..2541e6e 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -7,21 +7,24 @@ add_subdirectory(tools)
 file(GLOB headers *.h)
 file(GLOB_RECURSE tests code/*.cpp)
 
-include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../src ${JsonCpp_INCLUDE_DIRS})
+include_directories(
+    ${CMAKE_SOURCE_DIR}/src ${Boost_INCLUDE_DIRS} ${DCMTK_INCLUDE_DIRS}
+    ${JsonCpp_INCLUDE_DIRS})
 add_definitions(
     ${DCMTK_DEFINITIONS}
     -D BOOST_ASIO_DYN_LINK
     -D ODIL_MAJOR_VERSION=${odil_MAJOR_VERSION}
     -DBOOST_TEST_DYN_LINK
 )
-link_directories(${Boost_LIBRARY_DIRS})
+link_directories(${Boost_LIBRARY_DIRS} ${DCMTK_LIBRARY_DIRS})
 
 
 foreach(test_file ${tests})
     get_filename_component(test ${test_file} NAME_WE)
 
-    add_executable(${test} ${test_file} ${headers})
-    target_link_libraries(${test} odil ${Boost_LIBRARIES})
+    add_executable(test_${test} ${test_file} ${headers})
+    target_link_libraries(test_${test} libodil ${Boost_LIBRARIES})
+    set_target_properties(test_${test} PROPERTIES OUTPUT_NAME ${test})
 
     file(READ ${test_file} content)
 
@@ -52,4 +55,8 @@ foreach(test_file ${tests})
 
 endforeach()
 
-add_custom_target(Runner ${CMAKE_COMMAND} -E echo "Test runner" SOURCES run.sh)
+file(GLOB_RECURSE python_tests *.py)
+
+add_custom_target(Runner ${CMAKE_COMMAND} -E echo "Test runner" SOURCES run)
+add_custom_target(
+    PythonTests ${CMAKE_COMMAND} -E echo "Python tests" SOURCES ${python_tests})
diff --git a/tests/code/Association.cpp b/tests/code/Association.cpp
index e452cd1..5266ce8 100644
--- a/tests/code/Association.cpp
+++ b/tests/code/Association.cpp
@@ -9,44 +9,6 @@
 
 #include "../PeerFixtureBase.h"
 
-namespace odil
-{
-
-bool
-operator==(
-    AssociationParameters::PresentationContext const & left,
-    AssociationParameters::PresentationContext const & right)
-{
-    return (
-        left.abstract_syntax == right.abstract_syntax &&
-        left.transfer_syntaxes == right.transfer_syntaxes &&
-        left.scu_role_support == right.scu_role_support &&
-        left.scp_role_support == right.scp_role_support
-    );
-}
-
-bool
-operator==(
-    AssociationParameters const & left, AssociationParameters const & right)
-{
-    return (
-        left.get_calling_ae_title() == right.get_calling_ae_title() &&
-        left.get_called_ae_title() == right.get_called_ae_title() &&
-
-        left.get_presentation_contexts() == right.get_presentation_contexts() &&
-
-
-        left.get_user_identity().type == right.get_user_identity().type &&
-        left.get_user_identity().primary_field == right.get_user_identity().primary_field &&
-        left.get_user_identity().secondary_field == right.get_user_identity().secondary_field &&
-
-        left.get_maximum_length() == right.get_maximum_length()
-    );
-}
-
-}
-
-
 BOOST_AUTO_TEST_CASE(DefaultConstructor)
 {
     odil::Association association;
diff --git a/tests/code/DataSet.cpp b/tests/code/DataSet.cpp
index 414bb78..608537b 100644
--- a/tests/code/DataSet.cpp
+++ b/tests/code/DataSet.cpp
@@ -271,12 +271,11 @@ BOOST_AUTO_TEST_CASE(AddBinary)
     odil::Tag const tag("BadPixelImage");
 
     odil::DataSet dataset;
-    dataset.add(tag, odil::Value::Binary({0x01, 0x02}));
+    dataset.add(tag, odil::Value::Binary({{0x01, 0x02}}));
 
     BOOST_CHECK(dataset.is_binary(tag));
-    BOOST_REQUIRE_EQUAL(dataset.size(tag), 2);
     BOOST_REQUIRE(
-        dataset.as_binary(tag) == odil::Value::Binary({ 0x01, 0x02 }));
+        dataset.as_binary(tag) == odil::Value::Binary({{ 0x01, 0x02 }}));
 }
 
 BOOST_AUTO_TEST_CASE(ModifyInt)
diff --git a/tests/code/EchoSCU.cpp b/tests/code/EchoSCU.cpp
new file mode 100644
index 0000000..9a2eb0c
--- /dev/null
+++ b/tests/code/EchoSCU.cpp
@@ -0,0 +1,28 @@
+#define BOOST_TEST_MODULE EchoSCU
+#include <boost/test/unit_test.hpp>
+
+#include "odil/EchoSCU.h"
+#include "odil/registry.h"
+
+#include "../PeerFixtureBase.h"
+
+struct Fixture: public PeerFixtureBase
+{
+    Fixture()
+    : PeerFixtureBase({
+        {
+            1, odil::registry::VerificationSOPClass,
+            {odil::registry::ImplicitVRLittleEndian}, true, false
+        }
+    })
+    {
+        // Nothing else
+    }
+};
+
+BOOST_FIXTURE_TEST_CASE(Echo, Fixture)
+{
+    odil::EchoSCU scu(this->association);
+    scu.echo();
+}
+
diff --git a/tests/code/Reader.cpp b/tests/code/Reader.cpp
index 2999031..13a0106 100644
--- a/tests/code/Reader.cpp
+++ b/tests/code/Reader.cpp
@@ -134,7 +134,7 @@ BOOST_AUTO_TEST_CASE(IS)
 BOOST_AUTO_TEST_CASE(OB)
 {
     odil::Element odil_element(
-        std::vector<uint8_t>({0x01, 0x02, 0x03, 0x04}),
+        odil::Value::Binary({{0x01, 0x02, 0x03, 0x04}}),
         odil::VR::OB);
     odil::DataSet odil_data_set;
     odil_data_set.add(odil::registry::EncapsulatedDocument, odil_element);
@@ -145,7 +145,7 @@ BOOST_AUTO_TEST_CASE(OB)
 BOOST_AUTO_TEST_CASE(OF)
 {
     odil::Element odil_element(
-        std::vector<uint8_t>({0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}),
+        odil::Value::Binary({{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}),
         odil::VR::OF);
     odil::DataSet odil_data_set;
     odil_data_set.add(odil::registry::VectorGridData, odil_element);
@@ -156,7 +156,7 @@ BOOST_AUTO_TEST_CASE(OF)
 BOOST_AUTO_TEST_CASE(OW)
 {
     odil::Element odil_element(
-        std::vector<uint8_t>({0x01, 0x02, 0x03, 0x04}),
+        odil::Value::Binary({{0x01, 0x02, 0x03, 0x04}}),
         odil::VR::OW);
     odil::DataSet odil_data_set;
     odil_data_set.add(odil::registry::RedPaletteColorLookupTableData, odil_element);
diff --git a/tests/code/SCU.cpp b/tests/code/SCU.cpp
index 090d0ed..ba644a1 100644
--- a/tests/code/SCU.cpp
+++ b/tests/code/SCU.cpp
@@ -33,8 +33,3 @@ BOOST_FIXTURE_TEST_CASE(AffectedSOPClassUID, Fixture)
     BOOST_CHECK_EQUAL(scu.get_affected_sop_class(), "1.2.3");
 }
 
-BOOST_FIXTURE_TEST_CASE(Echo, Fixture)
-{
-    odil::SCU scu(this->association);
-    scu.echo();
-}
diff --git a/tests/code/Writer.cpp b/tests/code/Writer.cpp
index dc6a28b..ee13d3e 100644
--- a/tests/code/Writer.cpp
+++ b/tests/code/Writer.cpp
@@ -144,7 +144,7 @@ BOOST_AUTO_TEST_CASE(IS)
 BOOST_AUTO_TEST_CASE(OB)
 {
     odil::Element odil_element(
-        std::vector<uint8_t>({0x01, 0x02, 0x03, 0x04}),
+        odil::Value::Binary({{0x01, 0x02, 0x03, 0x04}}),
         odil::VR::OB);
     odil::DataSet odil_data_set;
     odil_data_set.add(odil::registry::EncapsulatedDocument, odil_element);
@@ -155,7 +155,7 @@ BOOST_AUTO_TEST_CASE(OB)
 BOOST_AUTO_TEST_CASE(OF)
 {
     odil::Element odil_element(
-        std::vector<uint8_t>({0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}),
+        odil::Value::Binary({{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}),
         odil::VR::OF);
     odil::DataSet odil_data_set;
     odil_data_set.add(odil::registry::VectorGridData, odil_element);
@@ -166,7 +166,7 @@ BOOST_AUTO_TEST_CASE(OF)
 BOOST_AUTO_TEST_CASE(OW)
 {
     odil::Element odil_element(
-        std::vector<uint8_t>({0x01, 0x02, 0x03, 0x04}),
+        odil::Value::Binary({{0x01, 0x02, 0x03, 0x04}}),
         odil::VR::OW);
     odil::DataSet odil_data_set;
     odil_data_set.add(odil::registry::RedPaletteColorLookupTableData, odil_element);
diff --git a/tests/code/json_converter.cpp b/tests/code/json_converter.cpp
index c4f9607..9cf7d3b 100644
--- a/tests/code/json_converter.cpp
+++ b/tests/code/json_converter.cpp
@@ -153,7 +153,7 @@ BOOST_AUTO_TEST_CASE(AsJSONBinary)
     odil::DataSet data_set;
     data_set.add(0xdeadbeef,
         odil::Element(
-            odil::Value::Binary({0x1, 0x2, 0x3, 0x4, 0x5}),
+            odil::Value::Binary({{0x1, 0x2, 0x3, 0x4, 0x5}}),
             odil::VR::OB));
 
     auto const json = odil::as_json(data_set);
@@ -275,5 +275,5 @@ BOOST_AUTO_TEST_CASE(AsDataSetBinary)
     BOOST_REQUIRE(data_set.get_vr("deadbeef") == odil::VR::OB);
     BOOST_REQUIRE(data_set.is_binary("deadbeef"));
     BOOST_REQUIRE(data_set.as_binary("deadbeef") == odil::Value::Binary(
-        {0x1, 0x2, 0x3, 0x4, 0x5}));
+        {{0x1, 0x2, 0x3, 0x4, 0x5}}));
 }
diff --git a/tests/code/xml_converter.cpp b/tests/code/xml_converter.cpp
index 840a7a7..bb40b70 100644
--- a/tests/code/xml_converter.cpp
+++ b/tests/code/xml_converter.cpp
@@ -515,7 +515,7 @@ BOOST_AUTO_TEST_CASE(AsXMLBinary)
     odil::DataSet data_set;
     data_set.add(0x00660023,
         odil::Element(
-            odil::Value::Binary({0x1, 0x2, 0x3, 0x4, 0x5}),
+            odil::Value::Binary({{0x1, 0x2, 0x3, 0x4, 0x5}}),
             odil::VR::OW));
 
     auto const xml = odil::as_xml(data_set);
@@ -613,17 +613,6 @@ BOOST_AUTO_TEST_CASE(AsXMLInvalidPersonName)
 }
 
 /******************************* TEST Error ************************************/
-BOOST_AUTO_TEST_CASE(AsXMLInvalidDICOMTag)
-{
-    odil::DataSet data_set;
-    data_set.add(0xbad00bad,
-        odil::Element(
-            odil::Value::Strings({"value"}),
-            odil::VR::CS));
-    BOOST_REQUIRE_THROW(odil::as_xml(data_set), odil::Exception);
-}
-
-/******************************* TEST Error ************************************/
 BOOST_AUTO_TEST_CASE(AsXMLBadVR)
 {
     odil::DataSet data_set;
@@ -929,7 +918,7 @@ BOOST_AUTO_TEST_CASE(AsDataSetBinary)
     BOOST_REQUIRE(data_set.get_vr("00660023") == odil::VR::OW);
     BOOST_REQUIRE(data_set.is_binary("00660023"));
     BOOST_REQUIRE(data_set.as_binary("00660023") == odil::Value::Binary(
-        {0x1, 0x2, 0x3, 0x4, 0x5}));
+        {{0x1, 0x2, 0x3, 0x4, 0x5}}));
 }
 
 /******************************* TEST Error ************************************/
diff --git a/tests/peer_fixture_base.py b/tests/peer_fixture_base.py
new file mode 100644
index 0000000..03a1690
--- /dev/null
+++ b/tests/peer_fixture_base.py
@@ -0,0 +1,18 @@
+import os
+import unittest
+
+import odil
+
+class PeerFixtureBase(unittest.TestCase):
+    def setUp(self, contexts):
+        self.association = odil.Association()
+        self.association.set_peer_host(os.environ["ODIL_PEER_HOST_NAME"])
+        self.association.set_peer_port(int(os.environ["ODIL_PEER_PORT"]))
+        self.association.update_parameters()\
+            .set_calling_ae_title(os.environ["ODIL_OWN_AET"])\
+            .set_called_ae_title(os.environ["ODIL_PEER_AET"]) \
+            .set_presentation_contexts(contexts)
+        self.association.associate()
+
+    def tearDown(self):
+        self.association.release()
diff --git a/tests/run b/tests/run
new file mode 100755
index 0000000..a2737e9
--- /dev/null
+++ b/tests/run
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+
+import argparse
+import os
+import re
+import shutil
+import subprocess
+import sys
+import time
+import tempfile
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Run all tests (C++ and Python). "
+            "This program must be run from the build directory.")
+    parser.add_argument("tests", nargs="*", help="Python only")
+    parser.add_argument("--no-network", action="store_true")
+    parser.add_argument("--test-regex", "-R", help="C++ only")
+    parser.add_argument("--exclude-regex", "-E", help="C++ only")
+    parser.add_argument("--exclude", "-e", help="Python only")
+    arguments = parser.parse_args()
+
+    if arguments.no_network:
+        excluded_cpp = [
+            "Association", "Network", "ServiceRole", "SCP", "SCU", "Transport"]
+        excluded_python = ["scu"]
+
+        arguments.exclude_regex = "{}{}{}".format(
+            arguments.exclude_regex or "",
+            "|" if arguments.exclude_regex else "",
+            "|".join(excluded_cpp))
+
+        arguments.exclude = "{}{}{}".format(
+            arguments.exclude or "",
+            "|" if arguments.exclude else "",
+            "|".join(excluded_python))
+
+    cpp_args = []
+    if arguments.test_regex:
+        cpp_args += ["-R", arguments.test_regex]
+    if arguments.exclude_regex:
+        cpp_args += ["-E", arguments.exclude_regex]
+
+    python_args = []
+    if arguments.exclude:
+        python_args += ["-e", arguments.exclude]
+    python_args += arguments.tests
+
+    directory, process = setUp(arguments.no_network)
+
+    environment = os.environ.copy()
+    environment["ODIL_OWN_AET"] = "LOCAL"
+    environment["ODIL_PEER_HOST_NAME"] = "127.0.0.1"
+    environment["ODIL_PEER_PORT"] = "11112"
+    environment["ODIL_PEER_AET"] = "REMOTE"
+    environment["PATH"] = os.pathsep.join(["tests/tools", environment["PATH"]])
+    environment["PYTHONPATH"] = os.pathsep.join(
+        ["wrappers", environment.get("PYTHONPATH", "")])
+
+    source_directory = None
+    with open("CMakeCache.txt") as fd:
+        lines = fd.readlines()
+        for line in lines:
+            match = re.match(r".*_SOURCE_DIR:.*=(.*)", line)
+            if match:
+                source_directory = match.group(1)
+                break
+
+    cpp_code = subprocess.call(
+        ["ctest", "--no-compress-output", "-T", "Test"]+cpp_args,
+        env=environment)
+    python_code = subprocess.call(
+        ["nosetests-2.7", os.path.join(source_directory, "tests", "wrappers")]+python_args,
+        env=environment)
+
+    tearDown(directory, process)
+
+    return max(cpp_code, python_code)
+
+def setUp(no_network):
+    directory = tempfile.mkdtemp()
+
+    process = None
+
+    if not no_network:
+        data = """
+HostTable BEGIN
+remote = (REMOTE, localhost, 11112)
+local = (LOCAL, localhost, 11113)
+HostTable END
+
+AETable BEGIN
+REMOTE {} RW (10, 1024mb) local
+AETable END
+        """.format(directory)
+        with open(os.path.join(directory, "config"), "w") as fd:
+            fd.write(data)
+
+        process = subprocess.Popen([
+            "dcmqrscp",
+            "-ll", "error",
+            "-c", os.path.join(directory, "config"),
+            "11112"])
+        time.sleep(1)
+
+        data = """
+            (0008,0016) UI =RawDataStorage
+            (0008,0018) UI [2.25.95090344942250266709587559073467305647]
+            (0010,0010) PN [Doe^John]
+            (0010,0020) LO [DJ001]
+        """
+
+        read, write = os.pipe()
+        os.write(write, data)
+        os.close(write)
+
+        subprocess.check_call(
+            [
+                "dump2dcm", "--write-xfer-little", "/dev/stdin",
+                os.path.join(directory, "data.dcm")
+            ],
+            stdin=read)
+        subprocess.check_call([
+            "storescu",
+            "-aet", "LOCAL", "-aec", "REMOTE",
+            "localhost", "11112",
+            os.path.join(directory, "data.dcm")])
+
+    return directory, process
+
+def tearDown(directory, process):
+    if process is not None:
+        process.terminate()
+        process.wait()
+    if directory is not None:
+        shutil.rmtree(directory)
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/tests/run.sh b/tests/run.sh
deleted file mode 100755
index 1177ea8..0000000
--- a/tests/run.sh
+++ /dev/null
@@ -1,57 +0,0 @@
-#! /bin/sh
-
-set -e
-set -u
-
-configure() {
-    cat > ${directory}/config  << EOF
-HostTable BEGIN
-remote = (REMOTE, localhost, 11112)
-local = (LOCAL, localhost, 11113)
-HostTable END
-
-AETable BEGIN
-REMOTE ${directory} RW (10, 1024mb) local
-AETable END
-EOF
-}
-
-add_data() {
-dump2dcm --write-xfer-little /dev/stdin ${directory}/data.dcm <<EOF
-(0008,0016) UI =RawDataStorage
-(0008,0018) UI [2.25.95090344942250266709587559073467305647]
-(0010,0010) PN [Doe^John]
-(0010,0020) LO [DJ001]
-EOF
-storescu -aet LOCAL -aec REMOTE localhost 11112 ${directory}/data.dcm
-}
-
-start_scp() {
-    dcmqrscp -c ${directory}/config 11112 &
-    PID=$!
-    sleep 1
-}
-
-stop_scp() {
-    kill ${PID}
-}
-
-clean() {
-    rm -rf ${directory}
-}
-
-directory=$(mktemp -d odil.XXX)
-configure
-start_scp
-add_data
-
-export ODIL_OWN_AET=LOCAL
-export ODIL_PEER_HOST_NAME=127.0.0.1
-export ODIL_PEER_PORT=11112
-export ODIL_PEER_AET=REMOTE
-
-ctest --no-compress-output -T Test $@ || true
-
-stop_scp
-clean
-
diff --git a/tests/tools/CMakeLists.txt b/tests/tools/CMakeLists.txt
index 9895042..35b445e 100644
--- a/tests/tools/CMakeLists.txt
+++ b/tests/tools/CMakeLists.txt
@@ -7,5 +7,6 @@ link_directories(${DCMTK_LIBRARY_DIRS})
 file(GLOB headers *.h)
 file(GLOB files "*.cc")
 
-add_executable(getscu ${files} ${headers})
-target_link_libraries(getscu ${DCMTK_LIBRARIES})
+add_executable(dcmtk_getscu ${files} ${headers})
+target_link_libraries(dcmtk_getscu ${DCMTK_LIBRARIES})
+set_target_properties(dcmtk_getscu PROPERTIES OUTPUT_NAME getscu)
diff --git a/tests/wrappers/test_association.py b/tests/wrappers/test_association.py
new file mode 100644
index 0000000..036ea9e
--- /dev/null
+++ b/tests/wrappers/test_association.py
@@ -0,0 +1,48 @@
+import unittest
+
+import odil
+
+class TestAssociation(unittest.TestCase):
+    def test_default_constructor(self):
+        association = odil.Association()
+        self.assertFalse(association.is_associated())
+
+    def test_peer_host(self):
+        association = odil.Association()
+        association.set_peer_host("pacs.example.com")
+        self.assertEqual(association.get_peer_host(), "pacs.example.com")
+
+    def test_peer_port(self):
+        association = odil.Association()
+        association.set_peer_port(1234)
+        self.assertEqual(association.get_peer_port(), 1234)
+
+    def test_peer_port(self):
+        association = odil.Association()
+        association.set_peer_port(1234)
+        self.assertEqual(association.get_peer_port(), 1234)
+
+    def test_parameters(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_called_ae_title("foo")
+
+        association = odil.Association()
+        association.set_parameters(parameters)
+        self.assertEqual(
+            association.get_parameters().get_called_ae_title(), "foo")
+
+    def test_update_parameters(self):
+        association = odil.Association()
+        association.update_parameters().set_called_ae_title("foo")
+        self.assertEqual(
+            association.get_parameters().get_called_ae_title(), "foo")
+
+    def test_next_message_id(self):
+        association = odil.Association()
+        id1 = association.next_message_id()
+        id2 = association.next_message_id()
+        self.assertNotEqual(id1, id2)
+
+if __name__ == "__main__":
+    unittest.main()
+
diff --git a/tests/wrappers/test_association_parameters.py b/tests/wrappers/test_association_parameters.py
new file mode 100644
index 0000000..fb4b8d0
--- /dev/null
+++ b/tests/wrappers/test_association_parameters.py
@@ -0,0 +1,116 @@
+import unittest
+
+import odil
+
+class TestAssociationParameters(unittest.TestCase):
+    def test_default_constructor(self):
+        parameters = odil.AssociationParameters()
+        self.assertEqual(parameters.get_called_ae_title(), "")
+        self.assertEqual(parameters.get_calling_ae_title(), "")
+        self.assertEqual(len(parameters.get_presentation_contexts()), 0)
+
+        user_identity = parameters.get_user_identity()
+        self.assertEqual(
+            user_identity.type, 
+            odil.AssociationParameters.UserIdentity.Type.None)
+
+        self.assertEqual(parameters.get_maximum_length(), 16384)
+
+    def test_called_ae_title(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_called_ae_title("foo")
+        self.assertEqual(parameters.get_called_ae_title(), "foo")
+
+    def test_calling_ae_title(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_calling_ae_title("foo")
+        self.assertEqual(parameters.get_calling_ae_title(), "foo")
+
+    def test_presentation_contexts(self):
+        presentation_context = odil.AssociationParameters.PresentationContext()
+        presentation_context.id = 1
+        presentation_context.abstract_syntax = "foo"
+        presentation_context.transfer_syntaxes.append("bar")
+
+        parameters = odil.AssociationParameters()
+        parameters.set_presentation_contexts([presentation_context])
+
+        self.assertEqual(len(parameters.get_presentation_contexts()), 1)
+        self.assertEqual(
+            parameters.get_presentation_contexts()[0], presentation_context)
+
+    def test_user_identity_username(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_user_identity_to_username("foo")
+
+        user_identity = parameters.get_user_identity()
+        self.assertEqual(
+            user_identity.type, 
+            odil.AssociationParameters.UserIdentity.Type.Username)
+        self.assertEqual(user_identity.primary_field, "foo")
+
+    def test_user_identity_username_and_password(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_user_identity_to_username_and_password("foo", "bar")
+
+        user_identity = parameters.get_user_identity()
+        self.assertEqual(
+            user_identity.type, 
+            odil.AssociationParameters.UserIdentity.Type.UsernameAndPassword)
+        self.assertEqual(user_identity.primary_field, "foo")
+        self.assertEqual(user_identity.secondary_field, "bar")
+
+    def test_user_identity_kerberos(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_user_identity_to_kerberos("foo")
+
+        user_identity = parameters.get_user_identity()
+        self.assertEqual(
+            user_identity.type, 
+            odil.AssociationParameters.UserIdentity.Type.Kerberos)
+        self.assertEqual(user_identity.primary_field, "foo")
+
+    def test_user_identity_saml(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_user_identity_to_saml("foo")
+
+        user_identity = parameters.get_user_identity()
+        self.assertEqual(
+            user_identity.type, 
+            odil.AssociationParameters.UserIdentity.Type.SAML)
+        self.assertEqual(user_identity.primary_field, "foo")
+
+    def test_user_identity_none(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_user_identity_to_saml("foo")
+        parameters.set_user_identity_to_none()
+
+        user_identity = parameters.get_user_identity()
+        self.assertEqual(
+            user_identity.type, 
+            odil.AssociationParameters.UserIdentity.Type.None)
+
+    def test_maximum_length(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_maximum_length(12345)
+        self.assertEqual(parameters.get_maximum_length(), 12345)
+
+    def test_chaining(self):
+        parameters = odil.AssociationParameters()
+        parameters.set_called_ae_title("foo").set_calling_ae_title("bar")
+        self.assertEqual(parameters.get_called_ae_title(), "foo")
+        self.assertEqual(parameters.get_calling_ae_title(), "bar")
+    
+class TestPresentationContext(unittest.TestCase):
+    def test_constructor(self):
+        presentation_context = odil.AssociationParameters.PresentationContext(
+            1, "foo", ["bar", "baz"], False, True)
+        self.assertEqual(presentation_context.id, 1)
+        self.assertEqual(presentation_context.abstract_syntax, "foo")
+        self.assertEqual(
+            [x for x in presentation_context.transfer_syntaxes], ["bar", "baz"])
+        self.assertEqual(presentation_context.scu_role_support, False)
+        self.assertEqual(presentation_context.scp_role_support, True)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_data_set.py b/tests/wrappers/test_data_set.py
new file mode 100644
index 0000000..c43e533
--- /dev/null
+++ b/tests/wrappers/test_data_set.py
@@ -0,0 +1,166 @@
+import unittest
+
+import odil
+
+class TestDataSet(unittest.TestCase):
+    def test_empty_constructor(self):
+        data_set = odil.DataSet()
+        self.assertTrue(data_set.empty())
+        self.assertEqual(data_set.size(), 0)
+        self.assertEqual(len(data_set), 0)
+
+    def test_empty_element(self):
+        tag = odil.registry.PatientName
+        data_set = odil.DataSet()
+        data_set.add(tag, odil.Element())
+
+        self.assertFalse(data_set.empty())
+        self.assertEqual(data_set.size(), 1)
+        self.assertEqual(len(data_set), 1)
+
+        self.assertEqual(data_set.get_vr(tag), odil.VR.INVALID)
+        self.assertTrue(data_set.empty(tag))
+        self.assertEqual(data_set.size(tag), 0)
+
+    def test_empty_element_tag(self):
+        tag = odil.registry.PatientName
+        data_set = odil.DataSet()
+        data_set.add(tag)
+
+        self.assertFalse(data_set.empty())
+        self.assertEqual(data_set.size(), 1)
+        self.assertEqual(len(data_set), 1)
+
+        self.assertEqual(data_set.get_vr(tag), odil.VR.PN)
+        self.assertTrue(data_set.empty(tag))
+        self.assertEqual(data_set.size(tag), 0)
+
+    def test_int_element(self):
+        tag = odil.registry.SelectorUSValue
+        value = [1, 2, 3]
+        data_set = odil.DataSet()
+        data_set.add(tag, odil.Value.Integers(value))
+
+        self.assertFalse(data_set.empty())
+        self.assertEqual(data_set.size(), 1)
+        self.assertEqual(len(data_set), 1)
+
+        self.assertEqual(data_set.get_vr(tag), odil.VR.US)
+        self.assertFalse(data_set.empty(tag))
+        self.assertEqual(data_set.size(tag), 3)
+        self.assertTrue(data_set.is_int(tag))
+        self.assertEqual([x for x in data_set.as_int(tag)], value)
+
+    def test_real_element(self):
+        tag = odil.registry.SelectorFLValue
+        value = [1.1, 2, 3.3]
+        data_set = odil.DataSet()
+        data_set.add(tag, odil.Value.Reals(value))
+
+        self.assertFalse(data_set.empty())
+        self.assertEqual(data_set.size(), 1)
+        self.assertEqual(len(data_set), 1)
+
+        self.assertEqual(data_set.get_vr(tag), odil.VR.FL)
+        self.assertFalse(data_set.empty(tag))
+        self.assertEqual(data_set.size(tag), 3)
+        self.assertTrue(data_set.is_real(tag))
+        self.assertEqual([x for x in data_set.as_real(tag)], value)
+
+    def test_string_element(self):
+        tag = odil.registry.SelectorCSValue
+        value = ["foo", "bar"]
+        data_set = odil.DataSet()
+        data_set.add(tag, odil.Value.Strings(value))
+
+        self.assertFalse(data_set.empty())
+        self.assertEqual(data_set.size(), 1)
+        self.assertEqual(len(data_set), 1)
+
+        self.assertEqual(data_set.get_vr(tag), odil.VR.CS)
+        self.assertFalse(data_set.empty(tag))
+        self.assertEqual(data_set.size(tag), 2)
+        self.assertTrue(data_set.is_string(tag))
+        self.assertEqual([x for x in data_set.as_string(tag)], value)
+
+    def test_data_set_element(self):
+        tag = odil.registry.SelectorCodeSequenceValue
+        value = [odil.DataSet(), odil.DataSet()]
+        data_set = odil.DataSet()
+        data_set.add(
+            tag, odil.Value.DataSets(value))
+
+        self.assertFalse(data_set.empty())
+        self.assertEqual(data_set.size(), 1)
+        self.assertEqual(len(data_set), 1)
+
+        self.assertEqual(data_set.get_vr(tag), odil.VR.SQ)
+        self.assertFalse(data_set.empty(tag))
+        self.assertEqual(data_set.size(tag), 2)
+        self.assertTrue(data_set.is_data_set(tag))
+        self.assertEqual([x for x in data_set.as_data_set(tag)], value)
+
+    def test_string_binary(self):
+        tag = odil.registry.RedPaletteColorLookupTableData
+        value = [odil.Value.BinaryItem("\x01\x02")]
+        data_set = odil.DataSet()
+        data_set.add(tag, odil.Value.Binary(value))
+
+        self.assertFalse(data_set.empty())
+        self.assertEqual(data_set.size(), 1)
+        self.assertEqual(len(data_set), 1)
+
+        self.assertEqual(data_set.get_vr(tag), odil.VR.OW)
+        self.assertFalse(data_set.empty(tag))
+        self.assertEqual(data_set.size(tag), 1)
+        self.assertTrue(data_set.is_binary(tag))
+        self.assertEqual(
+            [x for x in data_set.as_binary(tag)[0]], [x for x in value[0]])
+
+    def test_getitem(self):
+        data_set = odil.DataSet()
+        data_set.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        self.assertEqual(
+            [x for x in data_set[odil.registry.PatientName].as_string()],
+            ["Doe^John"])
+        self.assertRaises(Exception, lambda x: data_set[odil.registry.PatientID])
+
+    def test_iter(self):
+        data_set = odil.DataSet()
+        data_set.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        data_set.add(odil.registry.PatientID, odil.Value.Strings(["DJ123"]))
+        self.assertEqual(
+            [x.get_name() for x in data_set],
+            ["PatientName", "PatientID"])
+
+    def test_keys(self):
+        data_set = odil.DataSet()
+        data_set.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        data_set.add(odil.registry.PatientID, odil.Value.Strings(["DJ123"]))
+        self.assertEqual(
+            [x.get_name() for x in data_set.keys()],
+            ["PatientName", "PatientID"])
+
+    def test_values(self):
+        data_set = odil.DataSet()
+        data_set.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        data_set.add(odil.registry.PatientID, odil.Value.Strings(["DJ123"]))
+        self.assertEqual(
+            [
+                [item for item in element.as_string()] 
+                for element in data_set.values()],
+            [["Doe^John"], ["DJ123"]])
+
+    def test_items(self):
+        data_set = odil.DataSet()
+        data_set.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        data_set.add(odil.registry.PatientID, odil.Value.Strings(["DJ123"]))
+        self.assertEqual(
+            [
+                [tag.get_name(), [item for item in element.as_string()]] 
+                for tag, element in data_set.items()],
+            [["PatientName", ["Doe^John"]], ["PatientID", ["DJ123"]]])
+
+if __name__ == "__main__":
+    unittest.main()
+
diff --git a/tests/wrappers/test_echo_scu.py b/tests/wrappers/test_echo_scu.py
new file mode 100644
index 0000000..43575b9
--- /dev/null
+++ b/tests/wrappers/test_echo_scu.py
@@ -0,0 +1,26 @@
+import os
+import sys
+import unittest
+
+import odil
+
+sys.path.append(os.path.dirname(os.path.dirname(__file__)))
+from peer_fixture_base import PeerFixtureBase
+
+class TestEchoSCU(PeerFixtureBase):
+    def setUp(self):
+        PeerFixtureBase.setUp(
+            self, 
+            [
+                odil.AssociationParameters.PresentationContext(
+                    3, odil.registry.VerificationSOPClass,
+                    [ odil.registry.ImplicitVRLittleEndian ], True, False)
+            ]
+        )
+    
+    def test_echo(self):
+        echo = odil.EchoSCU(self.association)
+        echo.echo()
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_element.py b/tests/wrappers/test_element.py
new file mode 100644
index 0000000..1fcbe74
--- /dev/null
+++ b/tests/wrappers/test_element.py
@@ -0,0 +1,55 @@
+import unittest
+
+import odil
+
+class TestElement(unittest.TestCase):
+    def test_empty_constructor(self):
+        element = odil.Element()
+        self.assertTrue(element.empty())
+        self.assertEqual(element.size(), 0)
+        self.assertEqual(len(element), 0)
+        self.assertEqual(element.vr, odil.VR.INVALID)
+
+    def test_integers_constructor(self):
+        items = [1, 2, 3]
+        element = odil.Element(odil.Value.Integers(items), odil.VR.US)
+        self.assertEqual([x for x in element.as_int()], items)
+        self.assertEqual(element.vr, odil.VR.US)
+        self.assertEqual(element.size(), 3)
+        self.assertEqual(len(element), 3)
+
+    def test_reals_constructor(self):
+        items = [1.1, 2, 3.3]
+        element = odil.Element(odil.Value.Reals(items), odil.VR.FL)
+        self.assertEqual([x for x in element.as_real()], items)
+        self.assertEqual(element.vr, odil.VR.FL)
+        self.assertEqual(element.size(), 3)
+        self.assertEqual(len(element), 3)
+
+    def test_strings_constructor(self):
+        items = ["foo", "bar"]
+        element = odil.Element(odil.Value.Strings(items), odil.VR.CS)
+        self.assertEqual([x for x in element.as_string()], items)
+        self.assertEqual(element.vr, odil.VR.CS)
+        self.assertEqual(element.size(), 2)
+        self.assertEqual(len(element), 2)
+
+    def test_data_sets_constructor(self):
+        items = [odil.DataSet(), odil.DataSet()]
+        element = odil.Element(odil.Value.DataSets(items), odil.VR.SQ)
+        self.assertEqual([x for x in element.as_data_set()], items)
+        self.assertEqual(element.vr, odil.VR.SQ)
+        self.assertEqual(element.size(), 2)
+        self.assertEqual(len(element), 2)
+
+    def test_binary_constructor(self):
+        items = [odil.Value.BinaryItem("\x01\x02\x03")]
+        element = odil.Element(odil.Value.Binary(items), odil.VR.OB)
+        self.assertEqual(
+            [x for x in element.as_binary()[0]], [x for x in items[0]])
+        self.assertEqual(element.vr, odil.VR.OB)
+        self.assertEqual(element.size(), 1)
+        self.assertEqual(len(element), 1)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_elements_dictionary.py b/tests/wrappers/test_elements_dictionary.py
new file mode 100644
index 0000000..1afee09
--- /dev/null
+++ b/tests/wrappers/test_elements_dictionary.py
@@ -0,0 +1,48 @@
+import unittest
+
+import odil
+
+class TestElementsDictionaryKey(unittest.TestCase):
+    def test_empty_constructor(self):
+        key = odil.ElementsDictionaryKey()
+    
+    def test_tag_constructor(self):
+        key = odil.ElementsDictionaryKey(odil.registry.PatientName)
+    
+    def test_string_constructor(self):
+        key = odil.ElementsDictionaryKey("60xx0010")
+
+class TestElementsDictionaryEntry(unittest.TestCase):
+    def test_constructor(self):
+        entry = odil.ElementsDictionaryEntry(
+            "Patient's Name", "PatientName", "PN", "1")
+        self.assertEqual(entry.name, "Patient's Name")
+        self.assertEqual(entry.keyword, "PatientName")
+        self.assertEqual(entry.vr, "PN")
+        self.assertEqual(entry.vm, "1")
+
+class TestElementsDictionary(unittest.TestCase):
+    def test_contains(self):
+        self.assertTrue(
+            odil.registry.PatientName in odil.registry.public_dictionary)
+        self.assertFalse(
+            odil.Tag(0x0001, 0x0001) in odil.registry.public_dictionary)
+        self.assertTrue("60xx0010" in odil.registry.public_dictionary)
+        self.assertFalse("foo" in odil.registry.public_dictionary)
+    
+    def test_get_item_tag(self):
+        entry = odil.registry.public_dictionary[odil.registry.PatientName]
+        self.assertEqual(entry.name, "Patient's Name")
+        self.assertEqual(entry.keyword, "PatientName")
+        self.assertEqual(entry.vr, "PN")
+        self.assertEqual(entry.vm, "1")
+    
+    def test_get_item_string(self):
+        entry = odil.registry.public_dictionary["60xx0010"]
+        self.assertEqual(entry.name, "Overlay Rows")
+        self.assertEqual(entry.keyword, "OverlayRows")
+        self.assertEqual(entry.vr, "US")
+        self.assertEqual(entry.vm, "1")
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_find_scu.py b/tests/wrappers/test_find_scu.py
new file mode 100644
index 0000000..40eb177
--- /dev/null
+++ b/tests/wrappers/test_find_scu.py
@@ -0,0 +1,51 @@
+import os
+import sys
+import unittest
+
+import odil
+
+sys.path.append(os.path.dirname(os.path.dirname(__file__)))
+from peer_fixture_base import PeerFixtureBase
+
+class TestFindSCU(PeerFixtureBase):
+    def setUp(self):
+        PeerFixtureBase.setUp(
+            self, 
+            [
+                odil.AssociationParameters.PresentationContext(
+                1, odil.registry.PatientRootQueryRetrieveInformationModelFIND,
+                [ odil.registry.ImplicitVRLittleEndian ], True, False)
+            ]
+        )
+
+        self.query = odil.DataSet()
+        self.query.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        self.query.add(odil.registry.QueryRetrieveLevel, odil.Value.Strings(["PATIENT"]))
+        self.query.add(odil.registry.PatientID)
+
+    def test_without_callback(self):
+        find = odil.FindSCU(self.association)
+        find.set_affected_sop_class(odil.registry.PatientRootQueryRetrieveInformationModelFIND)
+        data_sets = find.find(self.query)
+
+        self.assertEqual(len(data_sets), 1)
+        self.assertEqual(
+            [x for x in data_sets[0].as_string(odil.registry.PatientID)],
+            ["DJ001"])
+
+    def test_with_callback(self):
+        data_sets = []
+        def callback(data_set):
+            data_sets.append(data_set)
+
+        find = odil.FindSCU(self.association)
+        find.set_affected_sop_class(odil.registry.PatientRootQueryRetrieveInformationModelFIND)
+        find.find(self.query, callback)
+
+        self.assertEqual(len(data_sets), 1)
+        self.assertEqual(
+            [x for x in data_sets[0].as_string(odil.registry.PatientID)],
+            ["DJ001"])
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_get_scu.py b/tests/wrappers/test_get_scu.py
new file mode 100644
index 0000000..7d2dd35
--- /dev/null
+++ b/tests/wrappers/test_get_scu.py
@@ -0,0 +1,54 @@
+import os
+import sys
+import unittest
+
+import odil
+
+sys.path.append(os.path.dirname(os.path.dirname(__file__)))
+from peer_fixture_base import PeerFixtureBase
+
+class TestGetSCU(PeerFixtureBase):
+    def setUp(self):
+        PeerFixtureBase.setUp(
+            self, 
+            [
+                odil.AssociationParameters.PresentationContext(
+                    1, odil.registry.PatientRootQueryRetrieveInformationModelGET,
+                    [ odil.registry.ImplicitVRLittleEndian ], True, False
+                ),
+                odil.AssociationParameters.PresentationContext(
+                    3, odil.registry.RawDataStorage,
+                    [ odil.registry.ImplicitVRLittleEndian ], False, True
+                )
+            ])
+
+        self.query = odil.DataSet()
+        self.query.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        self.query.add(odil.registry.QueryRetrieveLevel, odil.Value.Strings(["PATIENT"]))
+
+    def test_without_callback(self):
+        get = odil.GetSCU(self.association)
+        get.set_affected_sop_class(odil.registry.PatientRootQueryRetrieveInformationModelGET)
+        data_sets = get.get(self.query)
+
+        self.assertEqual(len(data_sets), 1)
+        self.assertEqual(
+            [x for x in data_sets[0].as_string(odil.registry.SOPInstanceUID)],
+            ["2.25.95090344942250266709587559073467305647"])
+
+    def test_with_callback(self):
+        data_sets = []
+        def callback(data_set):
+            data_sets.append(data_set)
+
+        get = odil.GetSCU(self.association)
+        get.set_affected_sop_class(odil.registry.PatientRootQueryRetrieveInformationModelGET)
+        get.get(self.query, callback)
+
+        self.assertEqual(len(data_sets), 1)
+        self.assertEqual(
+            [x for x in data_sets[0].as_string(odil.registry.SOPInstanceUID)],
+            ["2.25.95090344942250266709587559073467305647"])
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_move_scu.py b/tests/wrappers/test_move_scu.py
new file mode 100644
index 0000000..0092a57
--- /dev/null
+++ b/tests/wrappers/test_move_scu.py
@@ -0,0 +1,65 @@
+import os
+import sys
+import unittest
+
+import odil
+
+sys.path.append(os.path.dirname(os.path.dirname(__file__)))
+from peer_fixture_base import PeerFixtureBase
+
+class TestMoveSCU(PeerFixtureBase):
+    def setUp(self):
+        PeerFixtureBase.setUp(
+            self, 
+            [
+                odil.AssociationParameters.PresentationContext(
+                    1, odil.registry.PatientRootQueryRetrieveInformationModelMOVE,
+                    [ odil.registry.ImplicitVRLittleEndian ], True, False
+                ),
+                odil.AssociationParameters.PresentationContext(
+                    3, odil.registry.RawDataStorage,
+                    [ odil.registry.ImplicitVRLittleEndian ], False, True
+                )
+            ])
+
+        self.query = odil.DataSet()
+        self.query.add(odil.registry.PatientName, odil.Value.Strings(["Doe^John"]))
+        self.query.add(odil.registry.QueryRetrieveLevel, odil.Value.Strings(["PATIENT"]))
+    
+    def test_default_constructor(self):
+        move = odil.MoveSCU(self.association)
+        self.assertEqual(move.get_move_destination(), "")
+        
+    def test_move_destination(self):
+        move = odil.MoveSCU(self.association)
+        move.set_move_destination("remote")
+        self.assertEqual(move.get_move_destination(), "remote")
+
+    def test_without_callback(self):
+        move = odil.MoveSCU(self.association)
+        move.set_affected_sop_class(odil.registry.PatientRootQueryRetrieveInformationModelMOVE)
+        move.set_move_destination("LOCAL")
+        data_sets = move.move(self.query)
+
+        self.assertEqual(len(data_sets), 1)
+        self.assertEqual(
+            [x for x in data_sets[0].as_string(odil.registry.SOPInstanceUID)],
+            ["2.25.95090344942250266709587559073467305647"])
+
+    def test_with_callback(self):
+        data_sets = []
+        def callback(data_set):
+            data_sets.append(data_set)
+
+        move = odil.MoveSCU(self.association)
+        move.set_affected_sop_class(odil.registry.PatientRootQueryRetrieveInformationModelMOVE)
+        move.set_move_destination("LOCAL")
+        move.move(self.query, callback)
+
+        self.assertEqual(len(data_sets), 1)
+        self.assertEqual(
+            [x for x in data_sets[0].as_string(odil.registry.SOPInstanceUID)],
+            ["2.25.95090344942250266709587559073467305647"])
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_registry.py b/tests/wrappers/test_registry.py
new file mode 100644
index 0000000..71d0adb
--- /dev/null
+++ b/tests/wrappers/test_registry.py
@@ -0,0 +1,29 @@
+import unittest
+
+import odil
+
+class TestRegistry(unittest.TestCase):
+    def test_attribute(self):
+        tag = odil.registry.PatientID
+        self.assertEqual(tag.group, 0x0010)
+        self.assertEqual(tag.element, 0x0020)
+
+    def test_public_dictionary(self):
+        tag = odil.registry.PatientID
+        self.assertTrue(tag in odil.registry.public_dictionary)
+        entry = odil.registry.public_dictionary[tag]
+        self.assertEqual(entry.name, "Patient ID")
+        self.assertEqual(entry.keyword, "PatientID")
+        self.assertEqual(entry.vr, "LO")
+        self.assertEqual(entry.vm, "1")
+
+    def test_uids_dictionary(self):
+        uid = "1.2.840.10008.1.2.1"
+        self.assertTrue(uid in odil.registry.uids_dictionary)
+        entry = odil.registry.uids_dictionary[uid]
+        self.assertEqual(entry.name, "Explicit VR Little Endian")
+        self.assertEqual(entry.keyword, "ExplicitVRLittleEndian")
+        self.assertEqual(entry.type, "Transfer Syntax")
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_store_scu.py b/tests/wrappers/test_store_scu.py
new file mode 100644
index 0000000..20bb4fb
--- /dev/null
+++ b/tests/wrappers/test_store_scu.py
@@ -0,0 +1,52 @@
+import os
+import sys
+import unittest
+
+import odil
+
+sys.path.append(os.path.dirname(os.path.dirname(__file__)))
+from peer_fixture_base import PeerFixtureBase
+
+class TestStoreSCU(PeerFixtureBase):
+    def setUp(self):
+        PeerFixtureBase.setUp(
+            self, 
+            [
+                odil.AssociationParameters.PresentationContext(
+                    1, odil.registry.RawDataStorage,
+                    [ odil.registry.ImplicitVRLittleEndian ], True, False
+                )
+            ])
+
+        self.data_set = odil.DataSet()
+        self.data_set.add(
+            odil.registry.ImageType,
+            odil.Value.Strings(["ORIGINAL", "PRIMARY", "OTHER"]))
+        self.data_set.add(
+            odil.registry.PatientID, odil.Value.Strings(["1234"]))
+        self.data_set.add(
+            odil.registry.StudyInstanceUID,
+            odil.Value.Strings(["2.25.386726390606491051215227596277040710"]))
+        self.data_set.add(
+            odil.registry.SeriesInstanceUID,
+            odil.Value.Strings(["2.25.235367796740370588607388995952651763168"]))
+        self.data_set.add(
+            odil.registry.SOPClassUID,
+            odil.Value.Strings([odil.registry.RawDataStorage]))
+        self.data_set.add(
+            odil.registry.SOPInstanceUID,
+            odil.Value.Strings(["2.25.294312554735929033890522327215919068328"]))
+    
+    def test_affected_sop_class_uid(self):
+        store = odil.StoreSCU(self.association)
+        store.set_affected_sop_class(self.data_set)
+        self.assertEqual(store.get_affected_sop_class(), odil.registry.RawDataStorage)
+    
+    def test_store(self):
+        store = odil.StoreSCU(self.association)
+
+        store.set_affected_sop_class(self.data_set)
+        store.store(self.data_set)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_tag.py b/tests/wrappers/test_tag.py
new file mode 100644
index 0000000..cf21990
--- /dev/null
+++ b/tests/wrappers/test_tag.py
@@ -0,0 +1,87 @@
+import unittest
+
+import odil
+
+class TestTag(unittest.TestCase):
+    def test_two_ints_constructor(self):
+        tag = odil.Tag(0x1234, 0x5678)
+        self.assertEqual(tag.group, 0x1234)
+        self.assertEqual(tag.element, 0x5678)
+
+    def test_one_int_constructor(self):
+        tag = odil.Tag(0x12345678)
+        self.assertEqual(tag.group, 0x1234)
+        self.assertEqual(tag.element, 0x5678)
+
+    def test_string_constructor(self):
+        tag = odil.Tag("PatientID")
+        self.assertEqual(tag.group, 0x0010)
+        self.assertEqual(tag.element, 0x0020)
+
+    def test_group(self):
+        tag = odil.Tag(0x1234, 0x5678)
+        tag.group = 0x4321
+        self.assertEqual(tag.group, 0x4321)
+        self.assertEqual(tag.element, 0x5678)
+
+    def test_element(self):
+        tag = odil.Tag(0x1234, 0x5678)
+        tag.element = 0x8765
+        self.assertEqual(tag.group, 0x1234)
+        self.assertEqual(tag.element, 0x8765)
+
+    def test_is_private(self):
+        public = odil.Tag(0x1234, 0x5678)
+        self.assertTrue(not public.is_private())
+
+        private = odil.Tag(0x1235, 0x5678)
+        self.assertTrue(private.is_private())
+
+    def test_get_name(self):
+        tag = odil.Tag(0x0010, 0x0020)
+        self.assertEqual(tag.get_name(), "PatientID")
+
+    def test_equality(self):
+        tag1 = odil.Tag(0x1234, 0x5678)
+        tag2 = odil.Tag(0x4321, 0x8765)
+        self.assertTrue(tag1 == tag1)
+        self.assertFalse(tag1 == tag2)
+
+    def test_inequality(self):
+        tag1 = odil.Tag(0x1234, 0x5678)
+        tag2 = odil.Tag(0x4321, 0x8765)
+        self.assertFalse(tag1 != tag1)
+        self.assertTrue(tag1 != tag2)
+
+    def test_less(self):
+        tag1 = odil.Tag(0x1234, 0x5678)
+        tag2 = odil.Tag(0x4321, 0x8765)
+        self.assertTrue(tag1 < tag2)
+        self.assertFalse(tag2 < tag1)
+
+    def test_greater(self):
+        tag1 = odil.Tag(0x1234, 0x5678)
+        tag2 = odil.Tag(0x4321, 0x8765)
+        self.assertFalse(tag1 > tag2)
+        self.assertTrue(tag2 > tag1)
+
+    def test_less_or_equal(self):
+        tag1 = odil.Tag(0x1234, 0x5678)
+        tag2 = odil.Tag(0x4321, 0x8765)
+        self.assertTrue(tag1 <= tag2)
+        self.assertTrue(tag1 <= tag1)
+        self.assertFalse(tag2 <= tag1)
+
+    def test_greater_or_equal(self):
+        tag1 = odil.Tag(0x1234, 0x5678)
+        tag2 = odil.Tag(0x4321, 0x8765)
+        self.assertFalse(tag1 >= tag2)
+        self.assertTrue(tag1 >= tag1)
+        self.assertTrue(tag2 >= tag1)
+
+    def test_str(self):
+        tag = odil.Tag(0x1234, 0x5678)
+        self.assertEqual(str(tag), "12345678")
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_uid.py b/tests/wrappers/test_uid.py
new file mode 100644
index 0000000..a80ec89
--- /dev/null
+++ b/tests/wrappers/test_uid.py
@@ -0,0 +1,14 @@
+import unittest
+
+import odil
+
+class TestUID(unittest.TestCase):
+    def test_uid_prefix(self):
+        self.assertEqual(odil.uid_prefix, "1.2.826.0.1.3680043.9.5560")
+
+    def test_uid_prefix(self):
+        uid = odil.generate_uid()
+        self.assertEqual(uid[:len(odil.uid_prefix)], odil.uid_prefix)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_uids_dictionary.py b/tests/wrappers/test_uids_dictionary.py
new file mode 100644
index 0000000..8088e6a
--- /dev/null
+++ b/tests/wrappers/test_uids_dictionary.py
@@ -0,0 +1,21 @@
+import unittest
+
+import odil
+
+class TestUIDsDictionaryEntry(unittest.TestCase):
+    def test_constructor(self):
+        entry = odil.UIDsDictionaryEntry(
+            "MR Image Storage", "MRImageStorage",  "SOP Class")
+        self.assertEqual(entry.name, "MR Image Storage")
+        self.assertEqual(entry.keyword, "MRImageStorage")
+        self.assertEqual(entry.type, "SOP Class")
+
+class TestUIDsDictionary(unittest.TestCase):
+    def test_get_item(self):
+        entry = odil.registry.uids_dictionary["1.2.840.10008.5.1.4.1.1.4"]
+        self.assertEqual(entry.name, "MR Image Storage")
+        self.assertEqual(entry.keyword, "MRImageStorage")
+        self.assertEqual(entry.type, "SOP Class")
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/wrappers/test_value.py b/tests/wrappers/test_value.py
new file mode 100644
index 0000000..035b972
--- /dev/null
+++ b/tests/wrappers/test_value.py
@@ -0,0 +1,88 @@
+import unittest
+
+import odil
+
+class TestValue(unittest.TestCase):
+    def test_empty_constructor(self):
+        value = odil.Value()
+        self.assertTrue(value.empty())
+
+    def test_integers_constructor(self):
+        items = [1, 2, 3]
+        value = odil.Value(odil.Value.Integers(items))
+        self.assertEqual([x for x in value.as_integers()], items)
+
+    def test_reals_constructor(self):
+        items = [1.1, 2, 3.3]
+        value = odil.Value(odil.Value.Reals(items))
+        self.assertEqual([x for x in value.as_reals()], items)
+
+    def test_strings_constructor(self):
+        items = ["foo", "bar"]
+        value = odil.Value(odil.Value.Strings(items))
+        self.assertEqual([x for x in value.as_strings()], items)
+
+    def test_data_sets_constructor(self):
+        items = [odil.DataSet(), odil.DataSet()]
+        value = odil.Value(odil.Value.DataSets(items))
+        self.assertEqual([x for x in value.as_data_sets()], items)
+
+    def test_binary_constructor(self):
+        items = [odil.Value.BinaryItem("\x01\x02\x03")]
+        value = odil.Value(odil.Value.Binary(items))
+        self.assertEqual(
+            [x for x in value.as_binary()[0]], [x for x in items[0]])
+
+class TestValueIntegers(unittest.TestCase):
+    def test_empty_constructor(self):
+        data = odil.Value.Integers()
+        self.assertEqual([x for x in data], [])
+
+    def test_sequence_constructor(self):
+        items = [1, 2, 3]
+        data = odil.Value.Integers(items)
+        self.assertEqual([x for x in data], items)
+
+class TestValueReals(unittest.TestCase):
+    def test_empty_constructor(self):
+        data = odil.Value.Reals()
+        self.assertEqual([x for x in data], [])
+
+    def test_sequence_constructor(self):
+        items = [1.1, 2, 3.3]
+        data = odil.Value.Reals(items)
+        self.assertEqual([x for x in data], items)
+
+class TestValueStrings(unittest.TestCase):
+    def test_empty_constructor(self):
+        data = odil.Value.Strings()
+        self.assertEqual([x for x in data], [])
+
+    def test_sequence_constructor(self):
+        items = ["foo", "bar"]
+        data = odil.Value.Strings(items)
+        self.assertEqual([x for x in data], items)
+
+class TestValueDataSets(unittest.TestCase):
+    def test_empty_constructor(self):
+        data = odil.Value.DataSets()
+        self.assertEqual([x for x in data], [])
+
+    def test_sequence_constructor(self):
+        items = [odil.DataSet(), odil.DataSet()]
+        data = odil.Value.DataSets(items)
+        self.assertEqual([x for x in data], items)
+
+class TestValueBinary(unittest.TestCase):
+    def test_empty_constructor(self):
+        data = odil.Value.Binary()
+        self.assertEqual([x for x in data], [])
+
+    def test_sequence_constructor(self):
+        items = [odil.Value.BinaryItem("\x01\x02\x03")]
+        data = odil.Value.Binary(items)
+        self.assertEqual([x for x in data[0]], [x for x in items[0]])
+
+if __name__ == "__main__":
+    unittest.main()
+
diff --git a/tests/wrappers/test_vr.py b/tests/wrappers/test_vr.py
new file mode 100644
index 0000000..ef1660f
--- /dev/null
+++ b/tests/wrappers/test_vr.py
@@ -0,0 +1,12 @@
+import unittest
+
+import odil
+
+class TestVR(unittest.TestCase):
+    def test_string(self):
+        vr = odil.VR.AE
+        self.assertEqual(str(vr), "AE")
+
+if __name__ == "__main__":
+    unittest.main()
+
diff --git a/wrappers/Assocation.cpp b/wrappers/Assocation.cpp
new file mode 100644
index 0000000..9188ce5
--- /dev/null
+++ b/wrappers/Assocation.cpp
@@ -0,0 +1,62 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/Association.h"
+
+void wrap_Association()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    scope association_scope = class_<Association>("Association", init<>())
+        .def("get_peer_host", &Association::get_peer_host,
+            return_value_policy<copy_const_reference>())
+        .def("set_peer_host", &Association::set_peer_host)
+        .def("get_peer_port", &Association::get_peer_port)
+        .def("set_peer_port", &Association::set_peer_port)
+        .def(
+            "get_parameters", 
+            &Association::get_parameters, 
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "set_parameters", 
+            &Association::set_parameters, 
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "update_parameters", 
+            &Association::update_parameters, 
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "get_negotiated_parameters", 
+            &Association::get_negotiated_parameters, 
+            return_value_policy<reference_existing_object>()
+        )
+        // TCP timeout
+        // Message timeout
+        .def("is_associated", &Association::is_associated)
+        .def("associate", &Association::associate)
+        // Receive association
+        //.def("reject", &Association::reject)
+        .def("release", &Association::release)
+        .def("abort", &Association::abort)
+        // Receive message
+        // Send message
+        .def("next_message_id", &Association::next_message_id)
+    ;
+
+    enum_<Association::Result>("Result")
+        .value("Accepted", Association::Accepted)
+        .value("RejectedPermanent", Association::RejectedPermanent)
+        .value("RejectedTransient", Association::RejectedTransient)
+    ;
+}
diff --git a/wrappers/AssocationParameters.cpp b/wrappers/AssocationParameters.cpp
new file mode 100644
index 0000000..f545c5e
--- /dev/null
+++ b/wrappers/AssocationParameters.cpp
@@ -0,0 +1,240 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <memory>
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#include <boost/shared_ptr.hpp>
+
+#include "odil/AssociationParameters.h"
+
+namespace
+{
+
+boost::shared_ptr<odil::AssociationParameters::PresentationContext>
+presentation_context_constructor(
+    uint8_t id, std::string const & abstract_syntax,
+    boost::python::list const & transfer_syntaxes,
+    bool scu_role_support, bool scp_role_support)
+{
+    std::vector<std::string> transfer_syntaxes_cpp(boost::python::len(transfer_syntaxes));
+    for(int i = 0; i<boost::python::len(transfer_syntaxes); ++i)
+    {
+        transfer_syntaxes_cpp[i] = boost::python::extract<std::string>(transfer_syntaxes[i]);
+    }
+    auto presentation_context = new odil::AssociationParameters::PresentationContext({
+        id, abstract_syntax, transfer_syntaxes_cpp,
+        scu_role_support, scp_role_support
+    });
+    // Old versions of Boost.Python (Debian 7, Ubuntu 12.04) do not like 
+    // std::shared_ptr
+    return boost::shared_ptr<odil::AssociationParameters::PresentationContext>(
+        presentation_context);
+}
+
+boost::python::list
+get_presentation_contexts(odil::AssociationParameters const & parameters)
+{
+    boost::python::list presentation_contexts_python;
+    for(auto const & presentation_context: parameters.get_presentation_contexts())
+    {
+        presentation_contexts_python.append(presentation_context);
+    }
+
+    return presentation_contexts_python;
+}
+
+odil::AssociationParameters &
+set_presentation_contexts(
+    odil::AssociationParameters & parameters,
+    boost::python::list const & presentation_contexts)
+{
+    std::vector<odil::AssociationParameters::PresentationContext> 
+        presentation_contexts_cpp(boost::python::len(presentation_contexts));
+    for(int i = 0; i<boost::python::len(presentation_contexts); ++i)
+    {
+        presentation_contexts_cpp[i] = 
+            boost::python::extract<
+                odil::AssociationParameters::PresentationContext
+            >(presentation_contexts[i]);
+    }
+    parameters.set_presentation_contexts(presentation_contexts_cpp);
+    
+    return parameters;
+}
+
+}
+
+void wrap_AssociationParameters()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    scope association_parameters_scope =
+        class_<AssociationParameters>("AssociationParameters", init<>())
+        // Construct from PDU
+        .def(
+            "get_called_ae_title",
+            &AssociationParameters::get_called_ae_title,
+            return_value_policy<copy_const_reference>()
+        )
+        .def(
+            "set_called_ae_title",
+            &AssociationParameters::set_called_ae_title,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "get_calling_ae_title",
+            &AssociationParameters::get_calling_ae_title,
+            return_value_policy<copy_const_reference>()
+        )
+        .def(
+            "set_calling_ae_title",
+            &AssociationParameters::set_calling_ae_title,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "get_presentation_contexts",
+            &get_presentation_contexts
+        )
+        .def(
+            "set_presentation_contexts",
+            &set_presentation_contexts,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "get_user_identity", &AssociationParameters::get_user_identity,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "set_user_identity_to_none",
+            &AssociationParameters::set_user_identity_to_none,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "set_user_identity_to_username",
+            &AssociationParameters::set_user_identity_to_username,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "set_user_identity_to_username_and_password",
+            &AssociationParameters::set_user_identity_to_username_and_password,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "set_user_identity_to_kerberos",
+            &AssociationParameters::set_user_identity_to_kerberos,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "set_user_identity_to_saml",
+            &AssociationParameters::set_user_identity_to_saml,
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "get_maximum_length",
+            &AssociationParameters::get_maximum_length
+        )
+        .def(
+            "set_maximum_length",
+            &AssociationParameters::set_maximum_length,
+            return_value_policy<reference_existing_object>()
+        )
+    ;
+
+    {
+        scope presentation_context_scope =
+            class_<AssociationParameters::PresentationContext>("PresentationContext")
+            .def(
+                "__init__", 
+                make_constructor(&presentation_context_constructor))
+            .def_readwrite(
+                "id",
+                &AssociationParameters::PresentationContext::id
+            )
+            .def_readwrite(
+                "abstract_syntax",
+                &AssociationParameters::PresentationContext::abstract_syntax
+            )
+            .def_readwrite(
+                "transfer_syntaxes",
+                &AssociationParameters::PresentationContext::transfer_syntaxes
+            )
+            .def_readwrite(
+                "scu_role_support",
+                &AssociationParameters::PresentationContext::scu_role_support
+            )
+            .def_readwrite(
+                "scp_role_support",
+                &AssociationParameters::PresentationContext::scp_role_support
+            )
+            .def_readwrite(
+                "result",
+                &AssociationParameters::PresentationContext::result
+            )
+            .def(self == self)
+        ;
+
+        enum_<AssociationParameters::PresentationContext::Result>("Result")
+            .value(
+                "Acceptance", 
+                AssociationParameters::PresentationContext::Result::Acceptance
+            )
+            .value(
+                "UserRejection", 
+                AssociationParameters::PresentationContext::Result::UserRejection
+            )
+            .value(
+                "NoReason", 
+                AssociationParameters::PresentationContext::Result::NoReason
+            )
+            .value(
+                "AbstractSyntaxNotSupported", 
+                AssociationParameters::PresentationContext::Result::AbstractSyntaxNotSupported
+            )
+            .value(
+                "TransferSyntaxesNotSupported", 
+                AssociationParameters::PresentationContext::Result::TransferSyntaxesNotSupported
+            )
+        ;
+    }
+
+    {
+        scope user_identity_scope = 
+            class_<AssociationParameters::UserIdentity>("UserIdentity")
+            .def_readwrite(
+                "type", 
+                &AssociationParameters::UserIdentity::type
+            )
+            .def_readwrite(
+                "primary_field", 
+                &AssociationParameters::UserIdentity::primary_field
+            )
+            .def_readwrite(
+                "secondary_field", 
+                &AssociationParameters::UserIdentity::secondary_field
+            )
+            .def(self == self)
+        ;
+
+        enum_<AssociationParameters::UserIdentity::Type>("Type")
+            .value("None", AssociationParameters::UserIdentity::Type::None)
+            .value(
+                "Username", AssociationParameters::UserIdentity::Type::Username
+            )
+            .value(
+                "UsernameAndPassword", 
+                AssociationParameters::UserIdentity::Type::UsernameAndPassword
+            )
+            .value(
+                "Kerberos", AssociationParameters::UserIdentity::Type::Kerberos)
+            .value("SAML", AssociationParameters::UserIdentity::Type::SAML)
+        ;
+    }
+}
diff --git a/wrappers/CMakeLists.txt b/wrappers/CMakeLists.txt
new file mode 100644
index 0000000..a5a0de3
--- /dev/null
+++ b/wrappers/CMakeLists.txt
@@ -0,0 +1,19 @@
+find_package(Boost REQUIRED COMPONENTS python)
+find_package(JsonCpp REQUIRED)
+find_package(PythonLibs 2.7 REQUIRED)
+
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}/../src ${Boost_INCLUDE_DIRS}
+    ${JsonCpp_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS})
+link_directories(${Boost_LIBRARY_DIRS})
+
+file(GLOB_RECURSE files "*.cpp")
+python_add_module(pyodil SHARED ${files})
+set_target_properties(
+    pyodil PROPERTIES OUTPUT_NAME odil)
+if(APPLE)
+    set_target_properties(pyodil PROPERTIES SUFFIX ".so")
+endif()
+
+target_link_libraries(
+    pyodil ${Boost_LIBRARIES} ${JsonCpp_LIBRARIES} libodil ${PYTHON_LIBRARIES})
diff --git a/wrappers/DataSet.cpp b/wrappers/DataSet.cpp
new file mode 100644
index 0000000..8d079fe
--- /dev/null
+++ b/wrappers/DataSet.cpp
@@ -0,0 +1,225 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <iterator>
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/map_indexing_suite.hpp>
+
+#include "odil/DataSet.h"
+
+BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(add_overloads_2, odil::DataSet::add, 1, 2);
+BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(add_overloads_3, odil::DataSet::add, 2, 3);
+
+namespace
+{
+
+class ConstIteratorAdapter
+{
+public:
+    typedef odil::DataSet::const_iterator::difference_type difference_type;
+    typedef odil::DataSet::const_iterator::value_type::first_type value_type;
+    typedef odil::DataSet::const_iterator::value_type::first_type const * pointer;
+    typedef odil::DataSet::const_iterator::value_type::first_type const & reference;
+    typedef std::bidirectional_iterator_tag iterator_category;
+
+    ConstIteratorAdapter(odil::DataSet::const_iterator const & iterator)
+    : _iterator(iterator)
+    {
+        // Nothing else.
+    }
+
+    reference operator*() const
+    {
+        return this->_iterator->first;
+    }
+
+    ConstIteratorAdapter & operator++()
+    {
+        ++this->_iterator;
+        return *this;
+    }
+
+    ConstIteratorAdapter operator++(int)
+    {
+        ConstIteratorAdapter tmp(*this);
+        ++(*this);
+        return tmp;
+    }
+
+    ConstIteratorAdapter & operator--()
+    {
+        --this->_iterator;
+        return *this;
+    }
+
+    ConstIteratorAdapter operator--(int)
+    {
+        ConstIteratorAdapter tmp(*this);
+        --(*this);
+        return tmp;
+    }
+
+    bool operator==(ConstIteratorAdapter const & other)
+    {
+        return this->_iterator == other._iterator;
+    }
+
+    bool operator!=(ConstIteratorAdapter const & other)
+    {
+        return this->_iterator != other._iterator;
+    }
+
+private:
+    odil::DataSet::const_iterator _iterator;
+};
+
+ConstIteratorAdapter begin(odil::DataSet const & data_set)
+{
+    return ConstIteratorAdapter(data_set.begin());
+}
+
+ConstIteratorAdapter end(odil::DataSet const & data_set)
+{
+    return ConstIteratorAdapter(data_set.end());
+}
+
+boost::python::list keys(odil::DataSet const & data_set)
+{
+    boost::python::list result;
+    for(auto const & item: data_set)
+    {
+        result.append(item.first);
+    }
+
+    return result;
+}
+
+boost::python::list values(odil::DataSet const & data_set)
+{
+    boost::python::list result;
+    for(auto const & item: data_set)
+    {
+        result.append(item.second);
+    }
+
+    return result;
+}
+
+boost::python::list items(odil::DataSet const & data_set)
+{
+    boost::python::list result;
+    for(auto const & item: data_set)
+    {
+        result.append(boost::python::make_tuple(item.first, item.second));
+    }
+
+    return result;
+}
+
+}
+
+void wrap_DataSet()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+
+    class_<DataSet>("DataSet", init<>())
+        .def(
+            "add", 
+            static_cast<void (DataSet::*)(Tag const &, Element const &)>(
+                &DataSet::add))
+        .def(
+            "add", 
+            static_cast<void (DataSet::*)(Tag const &, VR)>(&DataSet::add),
+            add_overloads_2())
+        .def(
+            "add", 
+            static_cast<void (DataSet::*)(Tag const &, Value::Integers const &, VR)>(
+                &DataSet::add),
+            add_overloads_3())
+        .def(
+            "add", 
+            static_cast<void (DataSet::*)(Tag const &, Value::Reals const &, VR)>(
+                &DataSet::add),
+            add_overloads_3())
+        .def(
+            "add", 
+            static_cast<void (DataSet::*)(Tag const &, Value::Strings const &, VR)>(
+                &DataSet::add),
+            add_overloads_3())
+        .def(
+            "add", 
+            static_cast<void (DataSet::*)(Tag const &, Value::DataSets const &, VR)>(
+                &DataSet::add),
+            add_overloads_3())
+        .def(
+            "add", 
+            static_cast<void (DataSet::*)(Tag const &, Value::Binary const &, VR)>(
+                &DataSet::add),
+            add_overloads_3())
+        .def("remove", &DataSet::remove)
+        .def("has", &DataSet::has)
+        .def("empty", static_cast<bool (DataSet::*)() const>(&DataSet::empty))
+        .def(
+            "size", 
+            static_cast<std::size_t (DataSet::*)() const>(&DataSet::size))
+        .def("get_vr", &DataSet::get_vr)
+        .def(
+            "empty", 
+            static_cast<bool (DataSet::*)(Tag const &) const>(&DataSet::empty))
+        .def(
+            "size", 
+            static_cast<std::size_t (DataSet::*)(Tag const &) const>(
+                &DataSet::size))
+        .def(
+            "__getitem__", 
+            static_cast<Element & (DataSet::*)(Tag const&)>(&DataSet::operator[]),
+            return_value_policy<reference_existing_object>())
+        .def("is_int", &DataSet::is_int)
+        .def(
+            "as_int", 
+            static_cast<Value::Integers & (DataSet::*)(Tag const &)>(
+                &DataSet::as_int), 
+            return_value_policy<reference_existing_object>())
+        .def("is_real", &DataSet::is_real)
+        .def(
+            "as_real", 
+            static_cast<Value::Reals & (DataSet::*)(Tag const &)>(
+                &DataSet::as_real), 
+            return_value_policy<reference_existing_object>())
+        .def("is_string", &DataSet::is_string)
+        .def(
+            "as_string", 
+            static_cast<Value::Strings & (DataSet::*)(Tag const &)>(
+                &DataSet::as_string), 
+            return_value_policy<reference_existing_object>())
+        .def("is_data_set", &DataSet::is_data_set)
+        .def(
+            "as_data_set", 
+            static_cast<Value::DataSets & (DataSet::*)(Tag const &)>(
+                &DataSet::as_data_set), 
+            return_value_policy<reference_existing_object>())
+        .def("is_binary", &DataSet::is_binary)
+        .def(
+            "as_binary", 
+            static_cast<Value::Binary & (DataSet::*)(Tag const &)>(
+                &DataSet::as_binary), 
+            return_value_policy<reference_existing_object>())
+        .def("keys", &keys)
+        .def("__iter__", range(&begin, &end))
+        .def("values", &values)
+        .def("items", &items)
+        .def(self == self)
+        .def(self != self)
+        .def(
+            "__len__", 
+            static_cast<std::size_t (DataSet::*)() const>(&DataSet::size)) 
+    ;
+}
diff --git a/wrappers/EchoSCU.cpp b/wrappers/EchoSCU.cpp
new file mode 100644
index 0000000..1f048c0
--- /dev/null
+++ b/wrappers/EchoSCU.cpp
@@ -0,0 +1,33 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/EchoSCU.h"
+
+void wrap_EchoSCU()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<EchoSCU>("EchoSCU", init<Association &>())
+        .def(
+            "get_affected_sop_class",
+            &EchoSCU::get_affected_sop_class,
+            return_value_policy<copy_const_reference>()
+        )
+        .def(
+            "set_affected_sop_class",
+            &EchoSCU::set_affected_sop_class
+        )
+        .def(
+            "echo", 
+            &EchoSCU::echo
+        )
+    ;
+}
diff --git a/wrappers/Element.cpp b/wrappers/Element.cpp
new file mode 100644
index 0000000..55e01d2
--- /dev/null
+++ b/wrappers/Element.cpp
@@ -0,0 +1,63 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/DataSet.h"
+#include "odil/Element.h"
+#include "odil/Value.h"
+
+void wrap_Element()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    typedef Value::Integers & (Element::*AsIntegers)();
+    typedef Value::Reals & (Element::*AsReals)();
+    typedef Value::Strings & (Element::*AsStrings)();
+    typedef Value::DataSets & (Element::*AsDataSets)();
+    typedef Value::Binary & (Element::*AsBinary)();
+
+    class_<Element>("Element", init<>())
+        .def_readwrite("vr", &Element::vr)
+        .def(init<Value, optional<VR>>())
+        .def(init<Value::Integers, optional<VR>>())
+        .def(init<Value::Reals, optional<VR>>())
+        .def(init<Value::Strings, optional<VR>>())
+        .def(init<Value::DataSets, optional<VR>>())
+        .def(init<Value::Binary, optional<VR>>())
+        .def("empty", &Element::empty)
+        .def("size", &Element::size)
+        .def(
+            "get_value", &Element::get_value, 
+            return_value_policy<reference_existing_object>())
+        .def("is_int", &Element::is_int)
+        .def(
+            "as_int", AsIntegers(&Element::as_int), 
+            return_value_policy<reference_existing_object>())
+        .def("is_real", &Element::is_real)
+        .def(
+            "as_real", AsReals(&Element::as_real), 
+            return_value_policy<reference_existing_object>())
+        .def("is_string", &Element::is_string)
+        .def(
+            "as_string", AsStrings(&Element::as_string), 
+            return_value_policy<reference_existing_object>())
+        .def("is_data_set", &Element::is_data_set)
+        .def(
+            "as_data_set", AsDataSets(&Element::as_data_set), 
+            return_value_policy<reference_existing_object>())
+        .def("is_binary", &Element::is_binary)
+        .def(
+            "as_binary", AsBinary(&Element::as_binary), 
+            return_value_policy<reference_existing_object>())
+        .def(self == self)
+        .def(self != self)
+        .def("__len__", &Element::size)
+    ;
+}
diff --git a/wrappers/ElementsDictionary.cpp b/wrappers/ElementsDictionary.cpp
new file mode 100644
index 0000000..a8330fc
--- /dev/null
+++ b/wrappers/ElementsDictionary.cpp
@@ -0,0 +1,96 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/map_indexing_suite.hpp>
+
+#include <odil/ElementsDictionary.h>
+
+namespace
+{
+
+bool contains(odil::ElementsDictionary const & container, odil::Tag const & key)
+{
+    return container.find(key) != container.end();
+}
+
+bool contains(odil::ElementsDictionary const & container, std::string const & key)
+{
+    return container.find(key) != container.end();
+}
+
+odil::ElementsDictionaryEntry &
+get_item(odil::ElementsDictionary & container, odil::Tag const & key)
+{
+    odil::ElementsDictionary::iterator iterator = container.find(key);
+    if(iterator == container.end())
+    {
+        PyErr_SetString(PyExc_KeyError, "Invalid key");
+        boost::python::throw_error_already_set();
+    }
+    return iterator->second;
+}
+
+odil::ElementsDictionaryEntry &
+get_item(odil::ElementsDictionary & container, std::string const & key)
+{
+    odil::ElementsDictionary::iterator iterator = container.find(key);
+    if(iterator == container.end())
+    {
+        PyErr_SetString(PyExc_KeyError, "Invalid key");
+        boost::python::throw_error_already_set();
+    }
+    return iterator->second;
+}
+
+
+}
+
+void wrap_ElementsDictionary()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<ElementsDictionaryKey>("ElementsDictionaryKey", init<>())
+        .def(init<Tag>())
+        .def(init<std::string>())
+    ;
+
+    class_<ElementsDictionaryEntry>(
+        "ElementsDictionaryEntry",
+        init<std::string, std::string, std::string, std::string>()
+    )
+        .def_readwrite("name", &ElementsDictionaryEntry::name)
+        .def_readwrite("keyword", &ElementsDictionaryEntry::keyword)
+        .def_readwrite("vr", &ElementsDictionaryEntry::vr)
+        .def_readwrite("vm", &ElementsDictionaryEntry::vm)
+    ;
+
+    class_<ElementsDictionary>("ElementsDictionary")
+        .def(map_indexing_suite<ElementsDictionary>())
+        .def(
+            "__contains__",
+            static_cast<bool(*)(ElementsDictionary const &, Tag const &)>(contains)
+        )
+        .def(
+            "__contains__",
+            static_cast<bool(*)(ElementsDictionary const &, std::string const &)>(contains)
+        )
+        .def(
+            "__getitem__",
+            static_cast<ElementsDictionaryEntry & (*)(ElementsDictionary &, Tag const &)>(get_item),
+            return_value_policy<reference_existing_object>()
+        )
+        .def(
+            "__getitem__",
+            static_cast<ElementsDictionaryEntry & (*)(ElementsDictionary &, std::string const &)>(get_item),
+            return_value_policy<reference_existing_object>()
+        )
+        ;
+    ;
+}
diff --git a/wrappers/Exception.cpp b/wrappers/Exception.cpp
new file mode 100644
index 0000000..cc6085c
--- /dev/null
+++ b/wrappers/Exception.cpp
@@ -0,0 +1,25 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/Exception.h"
+
+void translator(odil::Exception const & e)
+{
+    PyErr_SetString(PyExc_UserWarning, e.what());
+}
+
+void wrap_Exception()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    register_exception_translator<Exception>(translator);
+}
+
diff --git a/wrappers/FindSCU.cpp b/wrappers/FindSCU.cpp
new file mode 100644
index 0000000..701363e
--- /dev/null
+++ b/wrappers/FindSCU.cpp
@@ -0,0 +1,50 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/FindSCU.h"
+
+namespace
+{
+
+void 
+find_with_python_callback(
+    odil::FindSCU const & scu, 
+    odil::DataSet const & query, boost::python::object const & f)
+{
+    scu.find(
+        query, 
+        [f](odil::DataSet const & data_set) 
+        { 
+            boost::python::call<void>(f.ptr(), data_set); 
+        }
+    );
+}
+
+}
+
+void wrap_FindSCU()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<FindSCU>("FindSCU", init<Association &>())
+        .def(
+            "find",
+            &find_with_python_callback
+        )
+        .def(
+            "find", 
+            static_cast<
+                std::vector<DataSet> (FindSCU::*)(DataSet const &) const
+            >(&FindSCU::find)
+        )
+        .def("set_affected_sop_class", &FindSCU::set_affected_sop_class)
+    ;
+}
diff --git a/wrappers/GetSCU.cpp b/wrappers/GetSCU.cpp
new file mode 100644
index 0000000..b954ad4
--- /dev/null
+++ b/wrappers/GetSCU.cpp
@@ -0,0 +1,50 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/GetSCU.h"
+
+namespace
+{
+
+void 
+get_with_python_callback(
+    odil::GetSCU const & scu, 
+    odil::DataSet const & query, boost::python::object const & f)
+{
+    scu.get(
+        query, 
+        [f](odil::DataSet const & data_set) 
+        { 
+            boost::python::call<void>(f.ptr(), data_set); 
+        }
+    );
+}
+
+}
+
+void wrap_GetSCU()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<GetSCU>("GetSCU", init<Association &>())
+        .def(
+            "get",
+            &get_with_python_callback
+        )
+        .def(
+            "get", 
+            static_cast<
+                std::vector<DataSet> (GetSCU::*)(DataSet const &) const
+            >(&GetSCU::get)
+        )
+        .def("set_affected_sop_class", &GetSCU::set_affected_sop_class)
+    ;
+}
diff --git a/wrappers/MoveSCU.cpp b/wrappers/MoveSCU.cpp
new file mode 100644
index 0000000..565c47d
--- /dev/null
+++ b/wrappers/MoveSCU.cpp
@@ -0,0 +1,59 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/MoveSCU.h"
+
+namespace
+{
+
+void 
+move_with_python_callback(
+    odil::MoveSCU const & scu, 
+    odil::DataSet const & query, boost::python::object const & f)
+{
+    scu.move(
+        query, 
+        [f](odil::DataSet const & data_set) 
+        { 
+            boost::python::call<void>(f.ptr(), data_set); 
+        }
+    );
+}
+
+}
+
+void wrap_MoveSCU()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<MoveSCU>("MoveSCU", init<Association &>())
+        .def(
+            "get_move_destination",
+            &MoveSCU::get_move_destination,
+            return_value_policy<copy_const_reference>()
+        )
+        .def(
+            "set_move_destination",
+            &MoveSCU::set_move_destination
+        )
+        .def(
+            "move",
+            &move_with_python_callback
+        )
+        .def(
+            "move", 
+            static_cast<
+                std::vector<DataSet> (MoveSCU::*)(DataSet const &) const
+            >(&MoveSCU::move)
+        )
+        .def("set_affected_sop_class", &MoveSCU::set_affected_sop_class)
+    ;
+}
diff --git a/wrappers/StoreSCU.cpp b/wrappers/StoreSCU.cpp
new file mode 100644
index 0000000..266d731
--- /dev/null
+++ b/wrappers/StoreSCU.cpp
@@ -0,0 +1,33 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/StoreSCU.h"
+
+void wrap_StoreSCU()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<StoreSCU>("StoreSCU", init<Association &>())
+        .def(
+            "get_affected_sop_class",
+            &StoreSCU::get_affected_sop_class,
+            return_value_policy<copy_const_reference>()
+        )
+        .def(
+            "set_affected_sop_class",
+            &StoreSCU::set_affected_sop_class
+        )
+        .def(
+            "store", 
+            &StoreSCU::store
+        )
+    ;
+}
diff --git a/wrappers/Tag.cpp b/wrappers/Tag.cpp
new file mode 100644
index 0000000..cf49c4e
--- /dev/null
+++ b/wrappers/Tag.cpp
@@ -0,0 +1,33 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/Tag.h"
+
+void wrap_Tag()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<Tag>("Tag", init<uint16_t, uint16_t>())
+        .def(init<uint32_t>())
+        .def(init<std::string>())
+        .def_readwrite("group", &Tag::group)
+        .def_readwrite("element", &Tag::element)
+        .def("is_private", &Tag::is_private)
+        .def("get_name", &Tag::get_name)
+        .def(self == self)
+        .def(self != self)
+        .def(self < self)
+        .def(self > self)
+        .def(self <= self)
+        .def(self >= self)
+        .def("__str__", &Tag::operator std::string)
+    ;
+}
diff --git a/wrappers/UIDsDictionary.cpp b/wrappers/UIDsDictionary.cpp
new file mode 100644
index 0000000..e085f80
--- /dev/null
+++ b/wrappers/UIDsDictionary.cpp
@@ -0,0 +1,32 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/map_indexing_suite.hpp>
+
+#include <odil/UIDsDictionary.h>
+
+void wrap_UIDsDictionary()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    class_<UIDsDictionaryEntry>(
+        "UIDsDictionaryEntry",
+        init<std::string, std::string, std::string>()
+    )
+        .def_readwrite("name", &UIDsDictionaryEntry::name)
+        .def_readwrite("keyword", &UIDsDictionaryEntry::keyword)
+        .def_readwrite("type", &UIDsDictionaryEntry::type)
+    ;
+
+    class_<UIDsDictionary>("UIDsDictionary")
+        .def(map_indexing_suite<UIDsDictionary>())
+    ;
+}
+
diff --git a/wrappers/VR.cpp b/wrappers/VR.cpp
new file mode 100644
index 0000000..1dcc4a6
--- /dev/null
+++ b/wrappers/VR.cpp
@@ -0,0 +1,52 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/VR.h"
+
+void wrap_VR()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    enum_<VR>("VR")
+        .value("UNKNOWN", VR::UNKNOWN)
+        .value("AE", VR::AE)
+        .value("AS", VR::AS)
+        .value("AT", VR::AT)
+        .value("CS", VR::CS)
+        .value("DA", VR::DA)
+        .value("DS", VR::DS)
+        .value("DT", VR::DT)
+        .value("FD", VR::FD)
+        .value("FL", VR::FL)
+        .value("IS", VR::IS)
+        .value("LO", VR::LO)
+        .value("LT", VR::LT)
+        .value("PN", VR::PN)
+        .value("OB", VR::OB)
+        .value("OF", VR::OF)
+        .value("OW", VR::OW)
+        .value("SH", VR::SH)
+        .value("SL", VR::SL)
+        .value("SQ", VR::SQ)
+        .value("SS", VR::SS)
+        .value("ST", VR::ST)
+        .value("TM", VR::TM)
+        .value("UC", VR::UC)
+        .value("UI", VR::UI)
+        .value("UL", VR::UL)
+        .value("UN", VR::UN)
+        .value("UR", VR::UR)
+        .value("US", VR::US)
+        .value("UT", VR::UT)
+        .value("INVALID", VR::INVALID)
+    ;
+}
+
diff --git a/wrappers/Value.cpp b/wrappers/Value.cpp
new file mode 100644
index 0000000..f689294
--- /dev/null
+++ b/wrappers/Value.cpp
@@ -0,0 +1,110 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#include <boost/shared_ptr.hpp>
+
+#include "odil/DataSet.h"
+#include "odil/Value.h"
+
+template<typename T, typename python_type=typename T::value_type>
+boost::shared_ptr<T> create_value(boost::python::object const & sequence)
+{
+    typedef typename T::value_type value_type;
+
+    std::vector<value_type> values(boost::python::len(sequence));
+    for(long i=0; i<boost::python::len(sequence); ++i)
+    {
+        boost::python::object item = sequence[i];
+        values[i] = boost::python::extract<python_type>(item);
+    }
+
+    // Old versions of Boost.Python (Debian 7, Ubuntu 12.04) do not like 
+    // std::shared_ptr
+    return boost::shared_ptr<T>(new T(values));
+}
+
+void wrap_Value()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    typedef Value::Integers & (Value::*AsIntegers)();
+    typedef Value::Reals & (Value::*AsReals)();
+    typedef Value::Strings & (Value::*AsStrings)();
+    typedef Value::DataSets & (Value::*AsDataSets)();
+    typedef Value::Binary & (Value::*AsBinary)();
+
+    // Define scope to enclose Integers, Reals, etc. in Value
+    scope value_scope = class_<Value>("Value", init<>())
+        .def(init<Value::Integers>())
+        .def(init<Value::Reals>())
+        .def(init<Value::Strings>())
+        .def(init<Value::DataSets>())
+        .def(init<Value::Binary>())
+        .def("get_type", &Value::get_type)
+        .def("empty", &Value::empty)
+        .def(
+            "as_integers", AsIntegers(&Value::as_integers), 
+            return_value_policy<reference_existing_object>())
+        .def(
+            "as_reals", AsReals(&Value::as_reals), 
+            return_value_policy<reference_existing_object>())
+        .def(
+            "as_strings", AsStrings(&Value::as_strings), 
+            return_value_policy<reference_existing_object>())
+        .def(
+            "as_data_sets", AsDataSets(&Value::as_data_sets), 
+            return_value_policy<reference_existing_object>())
+        .def(
+            "as_binary", AsBinary(&Value::as_binary), 
+            return_value_policy<reference_existing_object>())
+        .def(self == self)
+        .def(self != self)
+    ;
+
+    class_<Value::Integers>("Integers")
+        .def(init<>())
+        .def("__init__", make_constructor(create_value<Value::Integers>))
+        .def(vector_indexing_suite<Value::Integers>())
+    ;
+
+    class_<Value::Reals>("Reals")
+        .def(init<>())
+        .def("__init__", make_constructor(create_value<Value::Reals>))
+        .def(vector_indexing_suite<Value::Reals>())
+    ;
+
+    class_<Value::Strings>("Strings")
+        .def(init<>())
+        .def("__init__", make_constructor(create_value<Value::Strings>))
+        .def(vector_indexing_suite<Value::Strings>())
+    ;
+
+    class_<Value::DataSets>("DataSets")
+        .def(init<>())
+        .def("__init__", make_constructor(create_value<Value::DataSets>))
+        .def(vector_indexing_suite<Value::DataSets>())
+    ;
+
+    class_<Value::Binary::value_type>("BinaryItem")
+        .def(init<>())
+        .def(
+            "__init__",
+            make_constructor(create_value<Value::Binary::value_type, char>))
+        .def(vector_indexing_suite<Value::Binary::value_type>())
+    ;
+
+    class_<Value::Binary>("Binary")
+        .def(init<>())
+        .def("__init__", make_constructor(create_value<Value::Binary>))
+        .def(vector_indexing_suite<Value::Binary>())
+    ;
+}
+
diff --git a/wrappers/json_converter.cpp b/wrappers/json_converter.cpp
new file mode 100644
index 0000000..9f271fe
--- /dev/null
+++ b/wrappers/json_converter.cpp
@@ -0,0 +1,44 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include <json/json.h>
+
+#include "odil/DataSet.h"
+#include "odil/json_converter.h"
+
+namespace
+{
+
+std::string as_json(odil::DataSet const & data_set, bool pretty_print)
+{
+    auto const json = odil::as_json(data_set);
+
+    Json::Writer * writer = NULL;
+    if(pretty_print)
+    {
+        writer = new Json::StyledWriter();
+    }
+    else
+    {
+        writer = new Json::FastWriter();
+    }
+
+    auto const string = writer->write(json);
+    return string;
+}
+
+}
+
+void wrap_json_converter()
+{
+    using namespace boost::python;
+
+    def("as_json", &as_json);
+}
diff --git a/wrappers/odil.cpp b/wrappers/odil.cpp
new file mode 100644
index 0000000..1fea1e8
--- /dev/null
+++ b/wrappers/odil.cpp
@@ -0,0 +1,58 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+void wrap_Association();
+void wrap_AssociationParameters();
+void wrap_DataSet();
+void wrap_EchoSCU();
+void wrap_Element();
+void wrap_ElementsDictionary();
+void wrap_Exception();
+void wrap_FindSCU();
+void wrap_GetSCU();
+void wrap_json_converter();
+void wrap_MoveSCU();
+void wrap_read();
+void wrap_StoreSCU();
+void wrap_Tag();
+void wrap_uid();
+void wrap_UIDsDictionary();
+void wrap_Value();
+void wrap_VR();
+void wrap_write();
+void wrap_xml_converter();
+
+void wrap_registry();
+
+BOOST_PYTHON_MODULE(odil)
+{
+    wrap_Association();
+    wrap_AssociationParameters();
+    wrap_DataSet();
+    wrap_EchoSCU();
+    wrap_Element();
+    wrap_ElementsDictionary();
+    wrap_Exception();
+    wrap_FindSCU();
+    wrap_GetSCU();
+    wrap_json_converter();
+    wrap_MoveSCU();
+    wrap_read();
+    wrap_StoreSCU();
+    wrap_Tag();
+    wrap_uid();
+    wrap_UIDsDictionary();
+    wrap_Value();
+    wrap_VR();
+    wrap_write();
+    wrap_xml_converter();
+
+    wrap_registry();
+}
diff --git a/wrappers/read.cpp b/wrappers/read.cpp
new file mode 100644
index 0000000..ba76e85
--- /dev/null
+++ b/wrappers/read.cpp
@@ -0,0 +1,48 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <fstream>
+#include <string>
+
+#include <boost/python.hpp>
+
+#include "odil/Exception.h"
+#include "odil/Reader.h"
+
+namespace
+{
+
+boost::python::tuple 
+read(std::string const & path, bool keep_group_length=false)
+{
+    std::ifstream stream(path);
+    if(!stream)
+    {
+        throw odil::Exception("Could not open "+path);
+    }
+
+    auto const header_and_data_set = 
+        odil::Reader::read_file(stream, keep_group_length);
+
+    return boost::python::make_tuple(
+        header_and_data_set.first, header_and_data_set.second);
+}
+
+}
+
+BOOST_PYTHON_FUNCTION_OVERLOADS(read_overloads, read, 1, 2)
+
+void wrap_read()
+{
+    using namespace boost::python;
+
+    def(
+        "read", 
+        static_cast<boost::python::tuple (*)(std::string const &, bool)>(read),
+        read_overloads());
+}
diff --git a/wrappers/registry.cpp b/wrappers/registry.cpp
new file mode 100644
index 0000000..826a114
--- /dev/null
+++ b/wrappers/registry.cpp
@@ -0,0 +1,44 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include <odil/registry.h>
+
+namespace
+{
+
+class Dummy 
+{
+};
+
+}
+
+void wrap_registry()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    scope registry_scope = class_<Dummy>("registry");
+
+    for(auto const & entry: registry::public_dictionary)
+    {
+        if(entry.first.get_type() == ElementsDictionaryKey::Type::Tag)
+        {
+            registry_scope.attr(entry.second.keyword.c_str()) = entry.first.get_tag();
+        }
+    }
+
+    for(auto const & entry: registry::uids_dictionary)
+    {
+        registry_scope.attr(entry.second.keyword.c_str()) = entry.first;
+    }
+
+    registry_scope.attr("public_dictionary") = registry::public_dictionary;
+    registry_scope.attr("uids_dictionary") = registry::uids_dictionary;
+}
diff --git a/wrappers/uid.cpp b/wrappers/uid.cpp
new file mode 100644
index 0000000..cbdc8e7
--- /dev/null
+++ b/wrappers/uid.cpp
@@ -0,0 +1,22 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/python.hpp>
+
+#include "odil/uid.h"
+
+void wrap_uid()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    scope().attr("uid_prefix") = uid_prefix;
+    scope().attr("implementation_class_uid") = implementation_class_uid;
+    scope().attr("implementation_version_name") = implementation_version_name;
+    def("generate_uid", generate_uid);
+}
diff --git a/wrappers/write.cpp b/wrappers/write.cpp
new file mode 100644
index 0000000..e6d4b01
--- /dev/null
+++ b/wrappers/write.cpp
@@ -0,0 +1,72 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <fstream>
+#include <string>
+
+#include <boost/python.hpp>
+
+#include "odil/Exception.h"
+#include "odil/Writer.h"
+
+namespace
+{
+
+void
+write(
+    odil::DataSet const & data_set, std::string const & path,
+    odil::DataSet const & meta_information = odil::DataSet(),
+    std::string const & transfer_syntax = odil::registry::ExplicitVRLittleEndian,
+    odil::Writer::ItemEncoding item_encoding=odil::Writer::ItemEncoding::ExplicitLength,
+    bool use_group_length=false
+)
+{
+    std::ofstream stream(path);
+    if(!stream)
+    {
+        throw odil::Exception("Could not open "+path);
+    }
+
+    odil::Writer::write_file(data_set, stream, meta_information, 
+        transfer_syntax, item_encoding, use_group_length);
+}
+
+}
+
+class Dummy { };
+
+void wrap_write()
+{
+    using namespace boost::python;
+    using namespace odil;
+
+    {
+        scope writer_scope = class_<Dummy>("Writer");
+        enum_<Writer::ItemEncoding>("ItemEncoding")
+            .value("ExplicitLength", Writer::ItemEncoding::ExplicitLength)
+            .value("UndefinedLength", Writer::ItemEncoding::UndefinedLength)
+        ;
+    }
+
+    def(
+        "write", 
+        static_cast<
+            void (*)(
+                DataSet const &, std::string const &, DataSet const &, 
+                std::string const &, Writer::ItemEncoding, bool)
+        >(write),
+        (
+            arg("data_set"), arg("path"), 
+            arg("meta_information")=odil::DataSet(), 
+            arg("transfer_syntax")=odil::registry::ExplicitVRLittleEndian,
+            arg("item_encoding")=odil::Writer::ItemEncoding::ExplicitLength,
+            arg("use_group_length")=false
+        )
+    );
+}
+
diff --git a/wrappers/xml_converter.cpp b/wrappers/xml_converter.cpp
new file mode 100644
index 0000000..1b0054a
--- /dev/null
+++ b/wrappers/xml_converter.cpp
@@ -0,0 +1,45 @@
+/*************************************************************************
+ * odil - Copyright (C) Universite de Strasbourg
+ * Distributed under the terms of the CeCILL-B license, as published by
+ * the CEA-CNRS-INRIA. Refer to the LICENSE file or to
+ * http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html
+ * for details.
+ ************************************************************************/
+
+#include <boost/property_tree/xml_parser.hpp>
+#include <boost/python.hpp>
+
+#include "odil/DataSet.h"
+#include "odil/xml_converter.h"
+
+namespace
+{
+
+std::string as_xml(odil::DataSet const & data_set, bool pretty_print)
+{
+    auto const xml = odil::as_xml(data_set);
+    std::ostringstream stream;
+
+#if BOOST_VERSION >= 105600
+    typedef boost::property_tree::xml_writer_settings<std::string> SettingsType;
+#else
+    typedef boost::property_tree::xml_writer_settings<char> SettingsType;
+#endif
+
+    SettingsType settings;
+    if(pretty_print)
+    {
+        settings = SettingsType('\t', 1);
+    }
+    boost::property_tree::write_xml(stream, xml, settings);
+    return stream.str();
+}
+
+}
+
+void wrap_xml_converter()
+{
+    using namespace boost::python;
+
+    def("as_xml", &as_xml);
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/odil.git



More information about the debian-med-commit mailing list