changeset 779:7b29eaf4ab82 attach-custom-data

merged default -> attach-custom-data
author Alain Mazy <am@orthanc.team>
date Thu, 30 Jan 2025 17:38:39 +0100
parents e1b7654fb58d (current diff) 15669253744c (diff)
children dd7fcf28b86b
files NewTests/PostgresUpgrades/docker-compose.yml NewTests/PostgresUpgrades/downgrade.sh NewTests/PostgresUpgrades/run-integ-tests-from-docker.sh NewTests/PostgresUpgrades/test_pg_upgrades.py
diffstat 34 files changed, 1353 insertions(+), 435 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Wed Oct 09 11:07:09 2024 +0200
+++ b/.hgtags	Thu Jan 30 17:38:39 2025 +0100
@@ -45,3 +45,5 @@
 855c3720902a1dade9accf91571ee6719e0c1eb6 Orthanc-1.12.1
 ec657d1a62a6c5eebfe5255a8afe082e92d973c1 Orthanc-1.12.3
 7bfc8992ab8fc44bd811bc60ebf3332303bc87ed Orthanc-1.12.4
+847b3c6b360b9b0daeab327133703c60e14e51f0 Orthanc-1.12.5
+287aae544b3133f6cecdf768f5a09dacbd75cf91 Orthanc-1.12.6
--- a/CITATION.cff	Wed Oct 09 11:07:09 2024 +0200
+++ b/CITATION.cff	Thu Jan 30 17:38:39 2025 +0100
@@ -10,5 +10,5 @@
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.4
-date-released: 2024-06-05
+version: 1.12.6
+date-released: 2025-01-22
--- a/GenerateConfigurationForTests.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/GenerateConfigurationForTests.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -143,6 +143,8 @@
 config['RemoteAccessAllowed'] = True
 config['OverwriteInstances'] = True
 config['StableAge'] = 1
+config['LimitFindInstances'] = 20
+config['LimitFindResults'] = 10
 config['JobsHistorySize'] = 1000
 config['SynchronousCMove'] = False
 config['MediaArchiveSize'] = 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NewTests/CGet/docker-compose-c-get.yml	Thu Jan 30 17:38:39 2025 +0100
@@ -0,0 +1,69 @@
+services:
+
+  orthanc-a:
+    image: ${ORTHANC_IMAGE_UNDER_TESTS:-orthancteam/orthanc:latest}
+    container_name: orthanc-a
+    restart: unless-stopped
+    ports: ["8072:8042"]
+    volumes: ["storage-orthanc-a:/var/lib/orthanc/db"]
+    environment:
+      VERBOSE_STARTUP: "true"
+      VERBOSE_ENABLED: "true"
+      ORTHANC_JSON: |
+        {
+          "AuthenticationEnabled": false,
+          "DicomAet": "ORTHANCA",
+          "Name": "Orthanc A",
+          "OverwriteInstances": true,
+          
+          "DicomModalities": {
+            "b": {
+              "AET": "ORTHANCB",
+              "Port": 4242,
+              "Host": "orthanc-b"
+            },
+            "b-move": {
+              "AET": "ORTHANCB",
+              "Port": 4242,
+              "Host": "orthanc-b",
+              "RetrieveMethod": "C-MOVE"
+            },
+            "b-get": {
+              "AET": "ORTHANCB",
+              "Port": 4242,
+              "Host": "orthanc-b",
+              "RetrieveMethod": "C-GET"
+            }
+          }
+        }
+
+
+  orthanc-b:
+    # last version before C-GET SCU
+    image: orthancteam/orthanc:24.12.0
+    container_name: orthanc-b
+    restart: unless-stopped
+    ports: ["8073:8042"]
+    volumes: ["storage-orthanc-b:/var/lib/orthanc/db"]
+    environment:
+      VERBOSE_STARTUP: "true"
+      VERBOSE_ENABLED: "true"
+      ORTHANC_JSON: |
+        {
+          "AuthenticationEnabled": false,
+          "DicomAet": "ORTHANCB",
+          "Name": "Orthanc B",
+          "OverwriteInstances": true,
+          
+          "DicomModalities": {
+            "a": {
+              "AET": "ORTHANCA",
+              "Port": 4242,
+              "Host": "orthanc-a"
+            }
+          }
+        }
+
+volumes:
+  storage-orthanc-a:
+  storage-orthanc-b:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NewTests/CGet/get-scp.py	Thu Jan 30 17:38:39 2025 +0100
@@ -0,0 +1,104 @@
+import os
+
+from pydicom import dcmread
+from pydicom.dataset import Dataset
+
+from pydicom.uid import ImplicitVRLittleEndian, ExplicitVRLittleEndian
+from pynetdicom import AE, StoragePresentationContexts, evt, AllStoragePresentationContexts
+from pynetdicom.sop_class import PatientRootQueryRetrieveInformationModelGet, StudyRootQueryRetrieveInformationModelGet, MRImageStorage, CTImageStorage
+
+import logging
+
+# Configure logging
+logging.basicConfig(level=logging.DEBUG)
+
+def transform_to_transfer_syntax(dataset, target_transfer_syntax):
+    # Create a new dataset with the new transfer syntax
+    new_dataset = Dataset()
+    new_dataset.file_meta = Dataset()
+    new_dataset.file_meta.TransferSyntaxUID = target_transfer_syntax
+    new_dataset.update(dataset)
+    return new_dataset
+
+# Implement the handler for evt.EVT_C_GET
+def handle_get(event):
+    """Handle a C-GET request event."""
+    ds = event.identifier
+    if 'QueryRetrieveLevel' not in ds:
+        # Failure
+        yield 0xC000, None
+        return
+
+    # Import stored SOP Instances
+    instances = []
+    matching = []
+    fdir = '/home/alain/o/orthanc-tests/Database/Brainix/Epi'
+    for fpath in os.listdir(fdir):
+        instances.append(dcmread(os.path.join(fdir, fpath)))
+
+    if ds.QueryRetrieveLevel == 'PATIENT':
+        if 'PatientID' in ds:
+            matching = [
+                inst for inst in instances if inst.PatientID == ds.PatientID
+            ]
+    elif ds.QueryRetrieveLevel == 'STUDY':
+        if 'StudyInstanceUID' in ds:
+            matching = [
+                inst for inst in instances if inst.StudyInstanceUID == ds.StudyInstanceUID
+            ]
+
+    print(f"GET-SCP: instances to send: {len(instances)}")
+    # Yield the total number of C-STORE sub-operations required
+    yield len(instances)
+
+    # Yield the matching instances
+    for instance in matching:
+        # Check if C-CANCEL has been received
+        if event.is_cancelled:
+            yield (0xFE00, None)
+            return
+
+        # Pending
+        accepted_transfer_syntax = event.assoc.accepted_contexts[0].transfer_syntax
+
+        if accepted_transfer_syntax != instance.file_meta.TransferSyntaxUID:
+            transformed_instance = transform_to_transfer_syntax(instance, accepted_transfer_syntax)
+            yield (0xFF00, transformed_instance)
+        else:
+            yield (0xFF00, instance)
+        
+
+handlers = [(evt.EVT_C_GET, handle_get)]
+
+# Create application entity
+ae = AE("PYNETDICOM")
+
+accepted_transfer_syntaxes = [
+    '1.2.840.10008.1.2',  # Implicit VR Little Endian
+    '1.2.840.10008.1.2.1',  # Explicit VR Little Endian
+    '1.2.840.10008.1.2.2',  # Explicit VR Big Endian
+    '1.2.840.10008.1.2.4.50',  # JPEG Baseline (Process 1)
+    '1.2.840.10008.1.2.4.70',  # JPEG Lossless, Non-Hierarchical (Process 14)
+]
+
+# # Add the supported presentation contexts (Storage SCU)
+# ae.supported_contexts = StoragePresentationContexts
+
+# # Accept the association requestor's proposed SCP role in the
+# #   SCP/SCU Role Selection Negotiation items
+# for cx in ae.supported_contexts:
+#     cx.scp_role = True
+#     cx.scu_role = False
+
+# # Add a supported presentation context (QR Get SCP)
+ae.add_supported_context(PatientRootQueryRetrieveInformationModelGet)
+ae.add_supported_context(StudyRootQueryRetrieveInformationModelGet)
+# ae.add_supported_context(MRImageStorage, accepted_transfer_syntaxes, scu_role=True, scp_role=True)
+# ae.add_supported_context(CTImageStorage, accepted_transfer_syntaxes, scu_role=True, scp_role=True)
+
+
+for context in AllStoragePresentationContexts:
+    ae.add_supported_context(context.abstract_syntax, ImplicitVRLittleEndian)
+
+# Start listening for incoming association requests
+ae.start_server(("0.0.0.0", 11112), evt_handlers=handlers)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NewTests/CGet/test_cget.py	Thu Jan 30 17:38:39 2025 +0100
@@ -0,0 +1,69 @@
+import unittest
+import time
+import os
+import threading
+from helpers import OrthancTestCase, Helpers
+
+from orthanc_api_client import OrthancApiClient, ChangeType
+from orthanc_api_client import helpers as OrthancHelpers
+
+import pathlib
+import subprocess
+import glob
+here = pathlib.Path(__file__).parent.resolve()
+
+class TestCGet(OrthancTestCase):
+
+    @classmethod
+    def cleanup(cls):
+        os.chdir(here)
+        print("Cleaning old compose")
+        subprocesss_env = os.environ.copy()
+        subprocesss_env["ORTHANC_IMAGE_UNDER_TESTS"] = Helpers.orthanc_under_tests_docker_image
+        subprocess.run(["docker", "compose", "-f", "docker-compose-c-get.yml", "down", "-v", "--remove-orphans"], 
+                       env=subprocesss_env, check=True)
+
+    @classmethod
+    def compose_up(cls):
+        # print("Pullling containers")
+        # subprocesss_env = os.environ.copy()
+        # subprocesss_env["ORTHANC_IMAGE_UNDER_TESTS"] = Helpers.orthanc_under_tests_docker_image
+        # subprocess.run(["docker", "compose", "-f", "docker-compose-transfers-concurrency.yml", "pull"], 
+        #                env=subprocesss_env, check=True)
+
+        print("Compose up")
+        subprocesss_env = os.environ.copy()
+        subprocesss_env["ORTHANC_IMAGE_UNDER_TESTS"] = Helpers.orthanc_under_tests_docker_image
+        subprocess.run(["docker", "compose", "-f", "docker-compose-c-get.yml", "up", "-d"], 
+                       env=subprocesss_env, check=True)
+
+    @classmethod
+    def setUpClass(cls):
+        cls.cleanup()
+        cls.compose_up()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cleanup()
+        pass
+
+    def clean_start(self):
+        oa = OrthancApiClient("http://localhost:8072")
+        ob = OrthancApiClient("http://localhost:8073")
+
+        oa.wait_started()
+        ob.wait_started()
+
+        oa.delete_all_content()
+        ob.delete_all_content()
+
+        return oa, ob
+
+    def test_cget(self):
+
+        oa, ob = self.clean_start()
+
+        instances_ids = ob.upload_folder( here / "../../Database/Brainix")
+
+        oa.modalities.get_study(from_modality='b', dicom_id='2.16.840.1.113669.632.20.1211.10000357775')
+        self.assertEqual(len(instances_ids), len(oa.instances.get_all_ids()))       
--- a/NewTests/Concurrency/docker-compose-transfers-concurrency.yml	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/Concurrency/docker-compose-transfers-concurrency.yml	Thu Jan 30 17:38:39 2025 +0100
@@ -9,8 +9,12 @@
     ports: ["8062:8042"]
     volumes: ["storage-orthanc-a:/var/lib/orthanc/db"]
     environment:
-      # VERBOSE_ENABLED: "true"
+      VERBOSE_STARTUP: "true"
+      VERBOSE_ENABLED: "true"
       TRANSFERS_PLUGIN_ENABLED: "true"
+      # increase this timeout for large transfers (it is configured at 2sec by the default integration tests config)
+      ORTHANC__HTTP_TIMEOUT: "60"
+      ORTHANC__TRANSFERS__PEER_CONNECTIVITY_TIMEOUT: "10"
       # disable DICOMWEB to avoid the metadata cache to consume disk space after StableStudy -> difficult to compare disk sizes
       DICOM_WEB_PLUGIN_ENABLED: "false"
       ORTHANC__POSTGRESQL: |
@@ -39,8 +43,12 @@
     ports: ["8063:8042"]
     volumes: ["storage-orthanc-b:/var/lib/orthanc/db"]
     environment:
-      # VERBOSE_ENABLED: "true"
+      VERBOSE_STARTUP: "true"
+      VERBOSE_ENABLED: "true"
       TRANSFERS_PLUGIN_ENABLED: "true"
+      # increase this timeout for large transfers (it is configured at 2sec by the default integration tests config)
+      ORTHANC__HTTP_TIMEOUT: "60"
+      ORTHANC__TRANSFERS__PEER_CONNECTIVITY_TIMEOUT: "10"
       DICOM_WEB_PLUGIN_ENABLED: "false"
       ORTHANC__POSTGRESQL: |
         {
--- a/NewTests/Concurrency/test_concurrency.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/Concurrency/test_concurrency.py	Thu Jan 30 17:38:39 2025 +0100
@@ -176,43 +176,49 @@
         for t in workers:
             t.join()
 
-    # TODO: reactivate once 1.12.4 is released.  It needs this fix: https://orthanc.uclouvain.be/hg/orthanc/rev/acdb8d78bf99
-    # def test_concurrent_uploads_same_study(self):
-    #     if self.o.is_orthanc_version_at_least(1, 12, 4):
+    def test_concurrent_uploads_same_study(self):
+        if self.o.is_orthanc_version_at_least(1, 12, 4):
 
-    #         self.o.delete_all_content()
-    #         self.clear_storage(storage_name=self._storage_name)
+            self.o.delete_all_content()
+            self.clear_storage(storage_name=self._storage_name)
+
+            start_time = time.time()
+            workers_count = 20
+            repeat_count = 5
 
-    #         start_time = time.time()
-    #         workers_count = 20
-    #         repeat_count = 10
+            # massively reupload the same study multiple times with OverwriteInstances set to true
+            # Make sure the studies, series and instances are created only once
+            self.execute_workers(
+                worker_func=worker_upload_folder,
+                worker_args=(self.o._root_url, here / "../../Database/Knee", repeat_count,),
+                workers_count=workers_count)
 
-    #         # massively reupload the same study multiple times with OverwriteInstances set to true
-    #         # Make sure the studies, series and instances are created only once
-    #         self.execute_workers(
-    #             worker_func=worker_upload_folder,
-    #             worker_args=(self.o._root_url, here / "../../Database/Knee", repeat_count,),
-    #             workers_count=workers_count)
+            elapsed = time.time() - start_time
+            print(f"TIMING test_concurrent_uploads_same_study with {workers_count} workers and {repeat_count}x repeat: {elapsed:.3f} s")
+
+            self.assertTrue(self.o.is_alive())
 
-    #         elapsed = time.time() - start_time
-    #         print(f"TIMING test_concurrent_uploads_same_study with {workers_count} workers and {repeat_count}x repeat: {elapsed:.3f} s")
+            self.assertEqual(1, len(self.o.studies.get_all_ids()))
+            self.assertEqual(2, len(self.o.series.get_all_ids()))
+            self.assertEqual(50, len(self.o.instances.get_all_ids()))
 
-    #         self.assertTrue(self.o.is_alive())
+            # check the computed count tags
+            patients = self.o.get_json("/patients?requested-tags=NumberOfPatientRelatedInstances;NumberOfPatientRelatedSeries;NumberOfPatientRelatedStudies&expand=true")
+            self.assertEqual(50, int(patients[0]['RequestedTags']['NumberOfPatientRelatedInstances']))
+            self.assertEqual(2, int(patients[0]['RequestedTags']['NumberOfPatientRelatedSeries']))
+            self.assertEqual(1, int(patients[0]['RequestedTags']['NumberOfPatientRelatedStudies']))
 
-    #         self.assertEqual(1, len(self.o.studies.get_all_ids()))
-    #         self.assertEqual(2, len(self.o.series.get_all_ids()))
-    #         self.assertEqual(50, len(self.o.instances.get_all_ids()))
 
-    #         stats = self.o.get_json("statistics")
-    #         self.assertEqual(1, stats.get("CountPatients"))
-    #         self.assertEqual(1, stats.get("CountStudies"))
-    #         self.assertEqual(2, stats.get("CountSeries"))
-    #         self.assertEqual(50, stats.get("CountInstances"))
-    #         self.assertEqual(4118738, int(stats.get("TotalDiskSize")))
+            stats = self.o.get_json("statistics")
+            self.assertEqual(1, stats.get("CountPatients"))
+            self.assertEqual(1, stats.get("CountStudies"))
+            self.assertEqual(2, stats.get("CountSeries"))
+            self.assertEqual(50, stats.get("CountInstances"))
+            self.assertEqual(4118738, int(stats.get("TotalDiskSize")))
 
-    #         self.o.instances.delete(orthanc_ids=self.o.instances.get_all_ids())
+            self.o.instances.delete(orthanc_ids=self.o.instances.get_all_ids())
 
-    #         self.check_is_empty()
+            self.check_is_empty()
 
     def test_concurrent_anonymize_same_study(self):
         self.o.delete_all_content()
@@ -254,6 +260,13 @@
         self.assertEqual(2 * (1 + workers_count * repeat_count), count_changes(changes, ChangeType.NEW_SERIES))
         self.assertEqual(50 * (1 + workers_count * repeat_count), count_changes(changes, ChangeType.NEW_INSTANCE))
 
+        # check the computed count tags
+        patients = self.o.get_json("/patients?requested-tags=NumberOfPatientRelatedInstances;NumberOfPatientRelatedSeries;NumberOfPatientRelatedStudies&expand=true")
+        for patient in patients:
+            self.assertEqual(50, int(patient['RequestedTags']['NumberOfPatientRelatedInstances']))
+            self.assertEqual(2, int(patient['RequestedTags']['NumberOfPatientRelatedSeries']))
+            self.assertEqual(1, int(patient['RequestedTags']['NumberOfPatientRelatedStudies']))
+
         start_time = time.time()
 
         self.o.instances.delete(orthanc_ids=self.o.instances.get_all_ids())
@@ -284,6 +297,13 @@
 
             self.check_is_empty()
 
+        # let's upload it one more time and check the children counts
+        self.o.upload_folder(here / "../../Database/Knee")
+        patients = self.o.get_json("/patients?requested-tags=NumberOfPatientRelatedInstances;NumberOfPatientRelatedSeries;NumberOfPatientRelatedStudies&expand=true")
+        self.assertEqual(50, int(patients[0]['RequestedTags']['NumberOfPatientRelatedInstances']))
+        self.assertEqual(2, int(patients[0]['RequestedTags']['NumberOfPatientRelatedSeries']))
+        self.assertEqual(1, int(patients[0]['RequestedTags']['NumberOfPatientRelatedStudies']))
+
         elapsed = time.time() - start_time
         print(f"TIMING test_upload_delete_same_study_from_multiple_threads with {workers_count} workers and {repeat_count}x repeat ({overall_repeat}x): {elapsed:.3f} s")
 
--- a/NewTests/Concurrency/test_transfer.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/Concurrency/test_transfer.py	Thu Jan 30 17:38:39 2025 +0100
@@ -45,7 +45,7 @@
 
     @classmethod
     def tearDownClass(cls):
-        #cls.cleanup()
+        cls.cleanup()
         pass
 
     def clean_start(self):
@@ -63,7 +63,7 @@
     def test_push(self):
         oa, ob = self.clean_start()
 
-        populator = OrthancTestDbPopulator(oa, studies_count=5, random_seed=65)
+        populator = OrthancTestDbPopulator(oa, studies_count=2, series_count=2, instances_count=200, random_seed=65)
         populator.execute()
 
         all_studies_ids = oa.studies.get_all_ids()
@@ -82,6 +82,16 @@
                 
                 self.assertEqual(instances_count, ob.get_statistics().instances_count)
                 self.assertEqual(disk_size, ob.get_statistics().total_disk_size)
+
+                # check the computed count tags
+                studies = ob.get_json("/studies?requested-tags=NumberOfStudyRelatedInstances;NumberOfStudyRelatedSeries&expand=true")
+                for study in studies:
+                    instance_count_a = len(oa.studies.get_instances_ids(study["ID"]))
+                    instance_count_b = len(ob.studies.get_instances_ids(study["ID"]))
+                    self.assertEqual(instance_count_a, instance_count_b)
+                    self.assertEqual(instance_count_a, int(study['RequestedTags']['NumberOfStudyRelatedInstances']))
+                    self.assertEqual(2, int(study['RequestedTags']['NumberOfStudyRelatedSeries']))
+
                 ob.delete_all_content()
 
             elapsed = time.time() - start_time
@@ -91,7 +101,7 @@
     def test_pull(self):
         oa, ob = self.clean_start()
 
-        populator = OrthancTestDbPopulator(ob, studies_count=5, random_seed=65)
+        populator = OrthancTestDbPopulator(ob, studies_count=2, series_count=2, instances_count=200, random_seed=65)
         populator.execute()
 
         all_studies_ids = ob.studies.get_all_ids()
@@ -112,6 +122,16 @@
 
                 self.assertEqual(instances_count, oa.get_statistics().instances_count)
                 self.assertEqual(disk_size, oa.get_statistics().total_disk_size)
+
+                # check the computed count tags
+                studies = oa.get_json("/studies?requested-tags=NumberOfStudyRelatedInstances;NumberOfStudyRelatedSeries&expand=true")
+                for study in studies:
+                    instance_count_a = len(oa.studies.get_instances_ids(study["ID"]))
+                    instance_count_b = len(ob.studies.get_instances_ids(study["ID"]))
+                    self.assertEqual(instance_count_a, instance_count_b)
+                    self.assertEqual(instance_count_a, int(study['RequestedTags']['NumberOfStudyRelatedInstances']))
+                    self.assertEqual(2, int(study['RequestedTags']['NumberOfStudyRelatedSeries']))
+
                 oa.delete_all_content()
 
 
--- a/NewTests/Housekeeper/test_housekeeper.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/Housekeeper/test_housekeeper.py	Thu Jan 30 17:38:39 2025 +0100
@@ -76,7 +76,7 @@
         while not completed:
             print('-------------- waiting for housekeeper to finish processing')
             time.sleep(1)
-            housekeeper_status = cls.o.get_json("plugins/housekeeper/status")
+            housekeeper_status = cls.o.get_json("/plugins/housekeeper/status")
             completed = (housekeeper_status["LastProcessedConfiguration"]["StorageCompressionEnabled"] == True) \
                         and (housekeeper_status["LastChangeToProcess"] == housekeeper_status["LastProcessedChange"])
 
--- a/NewTests/PostgresUpgrades/docker-compose.yml	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/PostgresUpgrades/docker-compose.yml	Thu Jan 30 17:38:39 2025 +0100
@@ -17,9 +17,9 @@
       AC_AUTHENTICATION_ENABLED: "false"
 
   # Orthanc previous version
-  orthanc-pg-15-6rev2:
-    image: orthancteam/orthanc:24.9.1
-    container_name: orthanc-pg-15-6rev2
+  orthanc-pg-15-6rev3:
+    image: orthancteam/orthanc:25.1.1
+    container_name: orthanc-pg-15-6rev3
     depends_on: [pg-15]
     restart: unless-stopped
     ports: ["8052:8042"]
@@ -30,9 +30,9 @@
       ORTHANC__AUTHENTICATION_ENABLED: "false"
 
   # Orthanc previous version to run the integration tests
-  orthanc-pg-15-6rev2-for-integ-tests:
-    image: orthancteam/orthanc:24.9.1
-    container_name: orthanc-pg-15-6rev2-for-integ-tests
+  orthanc-pg-15-6rev3-for-integ-tests:
+    image: orthancteam/orthanc:25.1.1
+    container_name: orthanc-pg-15-6rev3-for-integ-tests
     depends_on: [pg-15]
     restart: unless-stopped
     ports: ["8053:8042"]
@@ -51,7 +51,7 @@
     image: jodogne/orthanc-tests
     container_name: orthanc-tests
     depends_on:
-      - orthanc-pg-15-6rev2-for-integ-tests
+      - orthanc-pg-15-6rev3-for-integ-tests
     volumes:
       - ../../:/tests/orthanc-tests
       - ./wait-for-it.sh:/scripts/wait-for-it.sh
--- a/NewTests/PostgresUpgrades/downgrade.sh	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/PostgresUpgrades/downgrade.sh	Thu Jan 30 17:38:39 2025 +0100
@@ -2,10 +2,13 @@
 
 pushd /scripts
 
+apt-get update && apt-get install -y wget mercurial
+hg clone https://orthanc.uclouvain.be/hg/orthanc-databases
 # TODO: change attach-custom-data by the plugin version number or "default" !
-apt-get update && apt-get install -y wget && wget https://orthanc.uclouvain.be/hg/orthanc-databases/raw-file/attach-custom-data/PostgreSQL/Plugins/SQL/Downgrades/Rev3ToRev2.sql
-psql -U postgres -f Rev3ToRev2.sql
+hg update -r attach-custom-data
+psql -U postgres -f /scripts/orthanc-databases/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql
+psql -U postgres -f /scripts/orthanc-databases/PostgreSQL/Plugins/SQL/Downgrades/Rev3ToRev2.sql
 
 # if you want to test a downgrade procedure, you may use this code ...
 # psql -U postgres -f downgrade.sql
-popd
\ No newline at end of file
+popd
--- a/NewTests/PostgresUpgrades/orthanc-for-integ-tests.json	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/PostgresUpgrades/orthanc-for-integ-tests.json	Thu Jan 30 17:38:39 2025 +0100
@@ -114,8 +114,8 @@
      "IngestTranscodingOfCompressed": true, 
      "IngestTranscodingOfUncompressed": true, 
      "JobsHistorySize": 1000, 
-     "LimitFindInstances": 0, 
-     "LimitFindResults": 0, 
+     "LimitFindInstances": 20, 
+     "LimitFindResults": 10, 
      "LoadPrivateDictionary": true, 
      "LogExportedResources": true, 
      "LuaScripts": [], 
--- a/NewTests/PostgresUpgrades/run-integ-tests-from-docker.sh	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/PostgresUpgrades/run-integ-tests-from-docker.sh	Thu Jan 30 17:38:39 2025 +0100
@@ -2,6 +2,6 @@
 
 set -ex
 
-/scripts/wait-for-it.sh orthanc-pg-15-6rev2-for-integ-tests:8042 -t 60
-# python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-6rev2-for-integ-tests --force --docker -- -v  Orthanc.test_lua_deadlock
-python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-6rev2-for-integ-tests --force --docker -- -v
+/scripts/wait-for-it.sh orthanc-pg-15-6rev3-for-integ-tests:8042 -t 60
+# python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-6rev3-for-integ-tests --force --docker -- -v  Orthanc.test_lua_deadlock
+python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-6rev3-for-integ-tests --force --docker -- -v
--- a/NewTests/PostgresUpgrades/test_pg_upgrades.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/PostgresUpgrades/test_pg_upgrades.py	Thu Jan 30 17:38:39 2025 +0100
@@ -38,16 +38,16 @@
         subprocess.run(["docker", "compose", "up", "pg-15", "-d"], check=True)
         wait_container_healthy("pg-15")
 
-        print("Launching Orthanc with 6rev2 DB")
-        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev2", "-d"], check=True)
+        print("Launching Orthanc with DB 6rev3")
+        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev3", "-d"], check=True)
 
         o = OrthancApiClient("http://localhost:8052")
         o.wait_started()
 
         instances = o.upload_folder(here / "../../Database/Knee")
 
-        print("Stopping Orthanc with 6rev2 DB")
-        subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-6rev2"], check=True)
+        print("Stopping Orthanc with DB 6rev3")
+        subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-6rev3"], check=True)
         time.sleep(2)
 
         print("Launching newest Orthanc")
@@ -115,12 +115,12 @@
         subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-under-tests"], check=True)
         time.sleep(2)
 
-        print("Downgrading Orthanc DB to 6rev2")
+        print("Downgrading Orthanc DB to 6rev3")
         subprocess.run(["docker", "exec", "pg-15", "./scripts/downgrade.sh"], check=True)
         time.sleep(2)
 
-        print("Launching previous Orthanc (DB 6rev2)")
-        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev2", "-d"], check=True)
+        print("Launching previous Orthanc (DB 6rev3)")
+        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev3", "-d"], check=True)
 
         o = OrthancApiClient("http://localhost:8052")
         o.wait_started()
@@ -135,10 +135,6 @@
         self.assertEqual(0, int(o.get_json('statistics')['TotalDiskSize']))
 
         print("run the integration tests after a downgrade")
-        # first create the containers (orthanc-tests + orthanc-pg-15-6rev2-for-integ-tests) so they know each other
-        subprocess.run(["docker", "compose", "create", "orthanc-tests"], check=True)
-
-        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev2-for-integ-tests", "-d"], check=True)
 
         o = OrthancApiClient("http://localhost:8053", user="alice", pwd="orthanctest")
         o.wait_started()
--- a/NewTests/README	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/README	Thu Jan 30 17:38:39 2025 +0100
@@ -50,8 +50,10 @@
 Run the Housekeeper tests with your locally build version and break between preparation
 and execution to allow you to start your debugger.
 
+//                         --orthanc_under_tests_exe=/home/alain/o/build/orthanc/Orthanc \
+
 python3 NewTests/main.py --pattern=Housekeeper.test_housekeeper.TestHousekeeper.test_before_after_reconstruction \
-                         --orthanc_under_tests_exe=/home/alain/o/build/orthanc/Orthanc \
+                         --orthanc_under_tests_docker_image=orthancteam/orthanc:current \
                          --orthanc_under_tests_http_port=8043 \
                          --plugin=/home/alain/o/build/orthanc/libHousekeeper.so \
                          --break_after_preparation
@@ -195,7 +197,7 @@
 
 
 Read Only PG:
---------------
+------------
 
 Run the Read Only tests with your locally build version and break before execution to allow you to start your debugger.
 
@@ -210,4 +212,12 @@
 
 python3 NewTests/main.py --pattern=ReadOnly.test_readonly_pg.TestReadOnlyPG.* \
                          --orthanc_under_tests_docker_image=orthancteam/orthanc:current \
-                         --orthanc_under_tests_http_port=8043
\ No newline at end of file
+                         --orthanc_under_tests_http_port=8043
+
+C-Get:
+-----
+
+with Docker:
+
+python3 NewTests/main.py --pattern=CGet.test_cget.TestCGet.* \
+                         --orthanc_under_tests_docker_image=orthancteam/orthanc-pre-release:2025.01.20
\ No newline at end of file
--- a/NewTests/requirements.txt	Wed Oct 09 11:07:09 2024 +0200
+++ b/NewTests/requirements.txt	Thu Jan 30 17:38:39 2025 +0100
@@ -1,3 +1,3 @@
-orthanc-api-client>=0.16.2
+orthanc-api-client>=0.18.0
 orthanc-tools>=0.13.0
 uvicorn
\ No newline at end of file
--- a/Plugins/CGet/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/CGet/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Plugins/DicomWeb/DicomWeb.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/DicomWeb/DicomWeb.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Plugins/DicomWeb/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/DicomWeb/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -6,8 +6,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -303,7 +303,7 @@
                                  { 'Resources' : [ 'nope' ],
                                    'Synchronous' : True }))  # inexisting resource
 
-        if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 18, 0):
+        if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 18, 0):
             l = 4   # "Server" has been added
         else:
             l = 3   # For >= 1.10.1
@@ -315,7 +315,7 @@
 
         self.assertEqual(l, len(r))
         self.assertEqual("0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918", r['Resources']['Studies'][0])
-        if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 18, 0):
+        if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 18, 0):
             self.assertEqual("sample", r['Server'])
 
         # series
@@ -607,7 +607,7 @@
         self.assertEqual(u'王^小東', pn['Value'][0]['Ideographic'])
 
         # new derivated test added later
-        if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 18, 0):
+        if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 18, 0):
             a = DoGet(ORTHANC, '/dicom-web/studies?StudyInstanceUID=1.3.6.1.4.1.5962.1.2.0.1175775771.5711.0')
             self.assertEqual(1, len(a))
             pn = a[0]['00100010']  # Patient name
@@ -670,7 +670,7 @@
         # WADO-RS RetrieveFrames shall transcode ExplicitBigEndian to ExplicitLittleEndian
         # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=219
         
-        if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 17, 0):
+        if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 17, 0):
 
             UploadInstance(ORTHANC, 'TransferSyntaxes/1.2.840.10008.1.2.2.dcm')
 
@@ -698,7 +698,7 @@
         self.assertTrue('00280010' in a[0])
         self.assertEqual(512, a[0]['00280010']['Value'][0])
 
-        if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 17, 0):
+        if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 17, 0):
             a = DoGet(ORTHANC, '/dicom-web/studies/1.2.840.113619.2.176.2025.1499492.7391.1171285944.390/series/1.2.840.113619.2.176.2025.1499492.7391.1171285944.394/instances?includefield=00081140')
             self.assertEqual(1, len(a))
             self.assertTrue('00081140' in a[0])
@@ -1729,7 +1729,7 @@
         })
         self.assertIn("https://my-domain/dicom-web", m[0][u'7FE00010']['BulkDataURI'])
 
-        if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 13, 1):
+        if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 13, 1):
             m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyId, headers= {
                 'X-Forwarded-Host': 'my-domain',
                 'X-Forwarded-Proto': 'https'
@@ -1764,7 +1764,7 @@
         self.assertEqual(1, len(m))
         self.assertEqual(studyUid, m[0]['0020000D']['Value'][0])
 
-        if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 13, 1) and IsOrthancVersionAbove(ORTHANC, 1, 12, 1):
+        if IsPluginVersionAtLeast(ORTHANC, "dicom-web", 1, 13, 1) and IsOrthancVersionAbove(ORTHANC, 1, 12, 1):
             # This fails on DICOMweb <= 1.13 because of the "; q=.2",
             # since multiple accepts were not supported
             # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=216
--- a/Plugins/Recycling/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/Recycling/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Plugins/Transfers/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/Transfers/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -6,8 +6,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Plugins/WSI/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/WSI/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 # -*- coding: utf-8 -*-
 
 
@@ -6,8 +6,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -144,6 +144,12 @@
     return tiff
 
 
+def IsWhite(self, image):
+    e = image.getextrema()
+    self.assertEqual(3, len(e))
+    return e == ((255, 255), (255, 255), (255, 255))
+
+
 class Orthanc(unittest.TestCase):
     def setUp(self):
         if (sys.version_info >= (3, 0)):
@@ -461,7 +467,7 @@
 
         self.assertEqual(3, len(info['sizes']))
         
-        if IsPluginVersionAbove(ORTHANC, "wsi", 2, 1, 0):   # https://orthanc.uclouvain.be/hg/orthanc-wsi/rev/9dc7f1e8716d
+        if IsPluginVersionAtLeast(ORTHANC, "wsi", 2, 1, 0):   # https://orthanc.uclouvain.be/hg/orthanc-wsi/rev/9dc7f1e8716d
             self.assertEqual(512, info['sizes'][2]['width'])
             self.assertEqual(512, info['sizes'][2]['height'])
             self.assertEqual(256, info['sizes'][1]['width'])
@@ -578,6 +584,149 @@
             self.assertEqual(height, info['tiles'][0]['height'])
             self.assertEqual([ 1 ], info['tiles'][0]['scaleFactors'])
 
+    def test_on_the_fly(self):
+        a = UploadInstance(ORTHANC, 'Implicit-vr-us-palette.dcm') ['ID']
+
+        self.assertRaises(Exception, lambda: DoGet(ORTHANC, '/wsi/frames-pyramids/%s/1' % a))
+
+        info = DoGet(ORTHANC, '/wsi/frames-pyramids/%s/0' % a)
+        self.assertEqual('#ffffff', info['BackgroundColor'])
+        self.assertEqual(0, info['FrameNumber'])
+        self.assertEqual(a, info['ID'])
+        self.assertEqual(2, len(info['Resolutions']))
+        self.assertEqual(1, info['Resolutions'][0])
+        self.assertEqual(2, info['Resolutions'][1])
+        self.assertEqual(2, len(info['Sizes']))
+        self.assertEqual(832, info['Sizes'][0][0])  # Default padding is (64,64) for an image size (800,600)
+        self.assertEqual(640, info['Sizes'][0][1])
+        self.assertEqual(416, info['Sizes'][1][0])
+        self.assertEqual(320, info['Sizes'][1][1])
+        self.assertEqual(2, info['TilesCount'][0][0])
+        self.assertEqual(2, info['TilesCount'][0][1])
+        self.assertEqual(1, info['TilesCount'][1][0])
+        self.assertEqual(1, info['TilesCount'][1][1])
+        self.assertEqual(512, info['TilesSizes'][0][0])
+        self.assertEqual(512, info['TilesSizes'][0][1])
+        self.assertEqual(512, info['TilesSizes'][1][0])
+        self.assertEqual(512, info['TilesSizes'][1][1])
+        self.assertEqual(832, info['TotalWidth'])
+        self.assertEqual(640, info['TotalHeight'])
+
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/0/0/0' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/0/1/0' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/0/0/1' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/0/1/1' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/0/1/2' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertTrue(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/0/2/1' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertTrue(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/0/2/2' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertTrue(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/1/0/0' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/1/0/1' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertTrue(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/1/1/0' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertTrue(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/frames-tiles/%s/0/1/1/1' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertTrue(IsWhite(self, tile))
+
+    def test_iiif_on_the_fly(self):
+        a = UploadInstance(ORTHANC, 'Implicit-vr-us-palette.dcm') ['ID']
+
+        uri = '/wsi/iiif/frames-pyramids/%s/0' % a
+        manifest = DoGet(ORTHANC, uri + '/manifest.json')
+
+        self.assertEqual('http://iiif.io/api/presentation/3/context.json', manifest['@context'])
+        self.assertEqual('http://localhost:8042%s/manifest.json' % uri, manifest['id'])
+
+        self.assertEqual(1, len(manifest['items']))
+        self.assertEqual(1, len(manifest['items'][0]['items']))
+        self.assertEqual(1, len(manifest['items'][0]['items'][0]['items']))
+
+        self.assertEqual('Manifest', manifest['type'])
+        self.assertEqual('Canvas', manifest['items'][0]['type'])
+        self.assertEqual('AnnotationPage', manifest['items'][0]['items'][0]['type'])
+        self.assertEqual('Annotation', manifest['items'][0]['items'][0]['items'][0]['type'])
+
+        self.assertEqual(' - US -  - ', manifest['label']['en'][0])
+        self.assertEqual('http://localhost:8042%s/canvas/p1' % uri, manifest['items'][0]['id'])
+
+        annotation = manifest['items'][0]['items'][0]
+        self.assertEqual('http://localhost:8042%s/page/p1/1' % uri, annotation['id'])
+        self.assertEqual('AnnotationPage', annotation['type'])
+        self.assertEqual(1, len(annotation['items']))
+
+        item = manifest['items'][0]['items'][0]['items'][0]
+        self.assertEqual('image/jpeg', item['body']['format'])
+        self.assertEqual('Image', item['body']['type'])
+        self.assertEqual(832, item['body']['width'])
+        self.assertEqual(640, item['body']['height'])
+        self.assertEqual('http://localhost:8042%s/full/max/0/default.jpg' % uri, item['body']['id'])
+        self.assertEqual(1, len(item['body']['service']))
+        self.assertEqual('http://localhost:8042%s' % uri, item['body']['service'][0]['id'])
+        self.assertEqual('level0', item['body']['service'][0]['profile'])
+        self.assertEqual('ImageService3', item['body']['service'][0]['type'])
+        self.assertEqual('http://localhost:8042%s/annotation/p1-image' % uri, item['id'])
+        self.assertEqual('painting', item['motivation'])
+        self.assertEqual('Annotation', item['type'])
+        self.assertEqual(manifest['items'][0]['id'], item['target'])
+
+        self.assertEqual(832, manifest['items'][0]['width'])  # Default padding is (64,64) for an image size (800,600)
+        self.assertEqual(640, manifest['items'][0]['height'])
+
+        info = DoGet(ORTHANC, uri + '/info.json')
+        self.assertEqual('http://iiif.io/api/image/3/context.json', info['@context'])
+        self.assertEqual('http://iiif.io/api/image', info['protocol'])
+        self.assertEqual('http://localhost:8042%s' % uri, info['id'])
+        self.assertEqual('level0', info['profile'])
+        self.assertEqual('ImageService3', info['type'])
+        self.assertEqual(832, info['width'])
+        self.assertEqual(640, info['height'])
+
+        self.assertEqual(2, len(info['sizes']))
+        self.assertEqual(320, info['sizes'][0]['height'])
+        self.assertEqual(416, info['sizes'][0]['width'])
+        self.assertEqual(640, info['sizes'][1]['height'])
+        self.assertEqual(832, info['sizes'][1]['width'])
+
+        self.assertEqual(1, len(info['tiles']))
+        self.assertEqual(512, info['tiles'][0]['width'])
+        self.assertEqual(512, info['tiles'][0]['height'])
+        self.assertEqual([1, 2], info['tiles'][0]['scaleFactors'])
+
+        # Those are the calls made by Mirador
+        tile = GetImage(ORTHANC, '/wsi/iiif/frames-pyramids/%s/0/full/416,320/0/default.jpg' % a)
+        self.assertEqual((416, 320), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/iiif/frames-pyramids/%s/0/0,0,512,512/512,512/0/default.jpg' % a)
+        self.assertEqual((512, 512), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/iiif/frames-pyramids/%s/0/512,0,320,512/320,512/0/default.jpg' % a)
+        self.assertEqual((320, 512), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/iiif/frames-pyramids/%s/0/0,512,512,128/512,128/0/default.jpg' % a)
+        self.assertEqual((512, 128), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+        tile = GetImage(ORTHANC, '/wsi/iiif/frames-pyramids/%s/0/512,512,320,128/320,128/0/default.jpg' % a)
+        self.assertEqual((320, 128), tile.size)
+        self.assertFalse(IsWhite(self, tile))
+
 try:
     print('\nStarting the tests...')
     unittest.main(argv = [ sys.argv[0] ] + args.options)
--- a/Plugins/WebDav/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/WebDav/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Plugins/Worklists/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Plugins/Worklists/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -6,8 +6,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/README	Wed Oct 09 11:07:09 2024 +0200
+++ b/README	Thu Jan 30 17:38:39 2025 +0100
@@ -156,6 +156,9 @@
 # python CheckDicomTls.py --force OrthancCheckClient
 
 
+To run the Recycling tests:
+# python2 Plugins/Recycling/Run.py --force
+
 
 (Option 2b) With Docker:
 
--- a/Tests/CheckDicomTls.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/CheckDicomTls.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Tests/CheckHttpServerSecurity.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/CheckHttpServerSecurity.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Tests/CheckIngestTranscoding.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/CheckIngestTranscoding.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Tests/CheckScuTranscoding.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/CheckScuTranscoding.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Tests/CheckZipStreams.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/CheckZipStreams.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -128,6 +128,8 @@
                 Assert(streaming == True or streaming == None)
 
                 try:
+                    #if (sys.version_info >= (3, 0)):
+                    #    z = bytearray(z, 'utf-8')
                     Toolbox.ParseArchive(z)
                     print('error, got valid archive')
                     queue.put(False)  # The archive is not corrupted as expected
--- a/Tests/Run.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/Run.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- a/Tests/Tests.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/Tests.py	Thu Jan 30 17:38:39 2025 +0100
@@ -6,8 +6,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -722,22 +722,33 @@
 
     def test_archive(self):
         UploadInstance(_REMOTE, 'Knee/T1/IM-0001-0001.dcm')
-        UploadInstance(_REMOTE, 'Knee/T2/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Knee/T2/IM-0001-0003.dcm')
         kneePatient = 'ca29faea-b6a0e17f-067743a1-8b778011-a48b2a17'
         kneeStudy = DoGet(_REMOTE, '/studies')[0]
         kneeSeries = DoGet(_REMOTE, '/series')[0]
 
         z = GetArchive(_REMOTE, '/patients/%s/archive' % kneePatient)
         self.assertEqual(2, len(z.namelist()))
-        self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T2W_TSE/MR000003.dcm', z.namelist())
+        else:
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
         z = GetArchive(_REMOTE, '/studies/%s/archive' % kneeStudy)
         self.assertEqual(2, len(z.namelist()))
-        self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T2W_TSE/MR000003.dcm', z.namelist())
+        else:
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
         z = GetArchive(_REMOTE, '/series/%s/archive' % kneeSeries)
         self.assertEqual(1, len(z.namelist()))
-        self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
+        else:
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
         UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-0001.dcm')
         brainixPatient = '16738bc3-e47ed42a-43ce044c-a3414a45-cb069bd0'
@@ -751,8 +762,12 @@
             'Resources' : [ brainixPatient, kneePatient ]
             })
         self.assertEqual(3, len(z.namelist()))
-        self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000000.dcm', z.namelist())
-        self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
+            self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000001.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
+        else:
+            self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000000.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
         z = PostArchive(_REMOTE, '/patients/%s/archive' % kneePatient, {
             'Synchronous' : True
@@ -764,16 +779,24 @@
             'Resources' : [ brainixStudy, kneeStudy ]
             })
         self.assertEqual(3, len(z.namelist()))
-        self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000000.dcm', z.namelist())
-        self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
+            self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000001.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
+        else:
+            self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000000.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
         # archive with 1 patient & 1 study
         z = PostArchive(_REMOTE, '/tools/create-archive', {
             'Resources' : [ brainixPatient, kneeStudy ]
             })
         self.assertEqual(3, len(z.namelist()))
-        self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000000.dcm', z.namelist())
-        self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
+            self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000001.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
+        else:
+            self.assertIn('5Yp0E BRAINIX/0 IRM crbrale neurocrne/MR sT2WFLAIR/MR000000.dcm', z.namelist())
+            self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
 
     def test_archive_with_patient_ids_collision(self):
@@ -1307,7 +1330,7 @@
         self.assertTrue('LastUpdate' in m)
 
         m = DoGet(_REMOTE, '/series/%s/metadata' % series)
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5):
             self.assertEqual(4, len(m))
             self.assertTrue('MainDicomSequences' in m)    # since RequestAttributeSequence is now in the MainDicomTags
         elif IsOrthancVersionAbove(_REMOTE, 1, 11, 0):
@@ -1567,7 +1590,7 @@
 
         series = DoGet(_REMOTE, '/series')[0]
         m = DoGet(_REMOTE, '/series/%s/metadata' % series)
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5):
             self.assertEqual(4, len(m))
             self.assertTrue('MainDicomSequences' in m)    # since RequestAttributeSequence is now in the MainDicomTags
         elif IsOrthancVersionAbove(_REMOTE, 1, 11, 0):
@@ -1979,12 +2002,12 @@
         
         self.assertTrue('0010,0010' in DoGet(_REMOTE, '/patients/%s/module' % p))
         self.assertTrue('PatientName' in DoGet(_REMOTE, '/patients/%s/module?simplify' % p))
-        self.assertTrue('0010,0010' in DoGet(_REMOTE, '/studies/%s/module-patient' % p))
-        self.assertTrue('PatientName' in DoGet(_REMOTE, '/studies/%s/module-patient?simplify' % p))
+        self.assertTrue('0010,0010' in DoGet(_REMOTE, '/studies/%s/module-patient' % s))
+        self.assertTrue('PatientName' in DoGet(_REMOTE, '/studies/%s/module-patient?simplify' % s))
         self.assertTrue('0008,1030' in DoGet(_REMOTE, '/studies/%s/module' % s))
         self.assertTrue('StudyDescription' in DoGet(_REMOTE, '/studies/%s/module?simplify' % s))
-        self.assertTrue('0008,103e' in DoGet(_REMOTE, '/series/%s/module' % p))
-        self.assertTrue('SeriesDescription' in DoGet(_REMOTE, '/series/%s/module?simplify' % p))
+        self.assertTrue('0008,103e' in DoGet(_REMOTE, '/series/%s/module' % t))
+        self.assertTrue('SeriesDescription' in DoGet(_REMOTE, '/series/%s/module?simplify' % t))
         self.assertTrue('0008,0018' in DoGet(_REMOTE, '/instances/%s/module' % a))
         self.assertTrue('SOPInstanceUID' in DoGet(_REMOTE, '/instances/%s/module?simplify' % a))
 
@@ -2111,8 +2134,38 @@
                                              'Query' : { 'StationName' : 'SMR4-MP3' }})
         self.assertEqual(1, len(a))
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5):
+            a = DoPost(_REMOTE, '/tools/count-resources', { 'Level' : 'Series',
+                                                            'CaseSensitive' : False,
+                                                            'Query' : { 'StationName' : 'SMR4-MP3' }})
+            self.assertEqual(1, len(a))
+            self.assertEqual(1, a['Count'])
+
 
     def test_rest_find(self):
+        def CheckFind(query, expectedAnswers, shouldThrow = False):
+            
+            if not shouldThrow:
+                a = DoPost(_REMOTE, '/tools/find', query)
+                self.assertEqual(expectedAnswers, len(a))
+                return a
+            else:
+                self.assertRaises(Exception, lambda: DoPost(_REMOTE, '/tools/find', query))
+
+            
+
+        def CheckCount(query, expectedAnswers, shouldThrow = False):
+            if not shouldThrow:
+                b = DoPost(_REMOTE, '/tools/count-resources', query)
+                self.assertEqual(1, len(b))
+                self.assertEqual(expectedAnswers, b['Count'])
+                return b
+            else:
+                self.assertRaises(Exception, lambda: DoPost(_REMOTE, '/tools/count-resources', query))
+
+            
+
+
         # Upload 12 instances
         for i in range(3):
             UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-000%d.dcm' % (i + 1))
@@ -2121,175 +2174,248 @@
             UploadInstance(_REMOTE, 'Knee/T2/IM-0001-000%d.dcm' % (i + 1))
 
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 
-                                                 'PatientName' : '*NE*',
-                                                 'StudyDate': '20080819'
-                                              }})
-        self.assertEqual(1, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 
-                                                 'PatientName' : '*NE*',
-                                                 'PatientBirthDate': '20080101-20081231',
-                                                 'PatientSex': '0000'
-                                              }})
-        self.assertEqual(1, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 
-                                                 'StudyInstanceUID' : '2.16.840.1.113669.632.20.121711.10000160881'
-                                              }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 
-                                                 'StudyInstanceUID' : '2.16.840.1.113669.632.20.121711.10000160881',
-                                                 'SeriesInstanceUID': '1.3.46.670589.11.17521.5.0.3124.2008081908564160709'
-                                              }})
-        self.assertEqual(3, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 
-                                                 'StudyDate' : '20080818-20080820',
-                                                 'Modality': 'MR'
-                                              }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 
-                                                 'StudyDate' : '20080818-',
-                                                 'ModalitiesInStudy': 'MR'
-                                              }})
-        self.assertEqual(1, len(a))
-
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Patient',
-                                             'CaseSensitive' : False,
-                                             'Query' : { 'PatientName' : 'BRAINIX' }})
-        self.assertEqual(1, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Patient',
-                                             'CaseSensitive' : False,
-                                             'Query' : { 'PatientName' : 'BRAINIX\\KNEE\\NOPE' }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Patient',
-                                             'CaseSensitive' : False,
-                                             'Query' : { 'PatientName' : '*n*' }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Patient',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 'PatientName' : '*n*' }})
-        self.assertEqual(0, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Expand' : True,
-                                             'Level' : 'Patient',
-                                             'CaseSensitive' : False,
-                                             'Query' : { 'PatientName' : '*ne*' }})
-        self.assertEqual(1, len(a))
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'PatientName' : '*NE*',
+                      'StudyDate': '20080819'
+                  }}
+        CheckFind(query, 1)
+        CheckCount(query, 1, True) # tools/count does not support CaseSensitive
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : False,
+                  'Query' : {
+                      'PatientName' : '*NE*',
+                      'StudyDate': '20080819'
+                  }}
+        CheckFind(query, 1)
+        CheckCount(query, 1)
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : False,
+                  'Query' : {
+                      'PatientName' : '*NE*',
+                      'StudyDate': '20080819'
+                  },
+                  'Since' : 1
+                  }
+        if HasExtendedFind(_REMOTE): # usage of 'Since' is not reliable without ExtendedFind
+            CheckFind(query, 0)
+            CheckCount(query, 1) # Since is ignored in tools/count-resources
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'PatientName' : '*NE*',
+                      'PatientBirthDate': '20080101-20081231',
+                      'PatientSex': '0000'
+                  }}
+        CheckFind(query, 1)
+        CheckCount(query, 1, True) # tools/count-resources does not support CaseSensitive
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'PatientName' : '*NE*',
+                      'PatientBirthDate': '20080101-20081231',
+                      'PatientSex': '0000'
+                  },
+                  'Since': 1}
+        if HasExtendedFind(_REMOTE): # usage of 'Since' is not reliable without ExtendedFind
+            CheckFind(query, 0, True)  # 'CaseSensitive' can not be combined with 'Since'
+            CheckCount(query, 0, True) # tools/count-resources does not support CaseSensitive
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'PatientName' : '*ne*',
+                      'PatientBirthDate': '20080101-20081231',
+                      'PatientSex': '0000'
+                  }
+                }
+        CheckFind(query, 0)
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'PatientName' : '*ne*',
+                      'PatientBirthDate': '20080101-20081231',
+                      'PatientSex': '0000'
+                  },
+                  'Since': 1}
+        CheckFind(query, 0, True)  # 'CaseSensitive' can not be combined with 'Since' when searching for lower case (because the DicomIdentifiers are stored in UPPERCASE)
+
+        query = { 'Level' : 'Series',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'StudyInstanceUID' : '2.16.840.1.113669.632.20.121711.10000160881'
+                  }}
+        CheckFind(query, 2)
+        CheckCount(query, 2, True) # tools/count-resources does not support CaseSensitive
+
+        query = { 'Level' : 'Instance',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'StudyInstanceUID' : '2.16.840.1.113669.632.20.121711.10000160881',
+                      'SeriesInstanceUID': '1.3.46.670589.11.17521.5.0.3124.2008081908564160709'
+                  }}
+        CheckFind(query, 3)
+        CheckCount(query, 3, True) # tools/count-resources does not support CaseSensitive
+
+        query = { 'Level' : 'Series',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'StudyDate' : '20080818-20080820',
+                      'Modality': 'MR'
+                  }}
+        CheckFind(query, 2)
+        CheckCount(query, 2, True) # tools/count-resources does not support CaseSensitive
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : True,
+                  'Query' : {
+                      'StudyDate' : '20080818-',
+                      'ModalitiesInStudy': 'MR'
+                  }}
+        CheckFind(query, 1)
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : False,
+                  'Query' : {
+                      'StudyDate' : '20080818-',
+                      'ModalitiesInStudy': 'MR'
+                  }, 
+                  'Since': 1}
+
+        if HasExtendedFind(_REMOTE): # usage of 'Since' is not reliable without ExtendedFind
+            CheckFind(query, 0)
+            CheckCount(query, 1) # Since is ignored in tools/count-resources
+
+        query = { 'Level' : 'Patient',
+                  'CaseSensitive' : False,
+                  'Query' : { 'PatientName' : 'BRAINIX' }}
+        CheckFind(query, 1)
+        CheckCount(query, 1)
+
+        query = { 'Level' : 'Patient',
+                  'CaseSensitive' : False,
+                  'Query' : { 'PatientName' : 'BRAINIX\\KNEE\\NOPE' }}
+        CheckFind(query, 2)
+        CheckCount(query, 2)
+
+        query = { 'Level' : 'Patient',
+                  'CaseSensitive' : False,
+                  'Query' : { 'PatientName' : '*n*' }}
+        CheckFind(query, 2)
+        CheckCount(query, 2)
+
+        query = { 'Level' : 'Patient',
+                  'CaseSensitive' : True,
+                  'Query' : { 'PatientName' : '*n*' }}
+        CheckFind(query, 0)
+        CheckCount(query, 0, True)   # "CaseSensitive" is not available in "/tools/count-resources"
+
+        query = { 'Expand' : True,
+                  'Level' : 'Patient',
+                  'CaseSensitive' : False,
+                  'Query' : { 'PatientName' : '*ne*' }}
+        a = CheckFind(query, 1)
         self.assertEqual('20080822', a[0]['MainDicomTags']['PatientBirthDate'])
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Patient',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 'PatientName' : '*ne*' }})
-        self.assertEqual(0, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 'PatientName' : '*NE*' }})
-        self.assertEqual(1, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 'PatientName' : '*NE*' }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
-                                             'CaseSensitive' : True,
-                                             'Query' : { 'PatientName' : '*NE*' }})
-        self.assertEqual(6, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Patient', 'Query' : { }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study', 'Query' : { }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series', 'Query' : { }})
-        self.assertEqual(4, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance', 'Query' : { }})
-        self.assertEqual(12, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '20061201-20061201' }})
-        self.assertEqual(1, len(a))
+        query = { 'Level' : 'Patient',
+                  'CaseSensitive' : True,
+                  'Query' : { 'PatientName' : '*ne*' }}
+        CheckFind(query, 0)
+
+        query = { 'Level' : 'Study',
+                  'CaseSensitive' : True,
+                  'Query' : { 'PatientName' : '*NE*' }}
+        CheckFind(query, 1)
+
+        query = { 'Level' : 'Series',
+                  'CaseSensitive' : True,
+                  'Query' : { 'PatientName' : '*NE*' }}
+        CheckFind(query, 2)
+
+        query = { 'Level' : 'Instance',
+                  'CaseSensitive' : True,
+                  'Query' : { 'PatientName' : '*NE*' }}
+        CheckFind(query, 6)
+
+        query = { 'Level' : 'Patient', 'Query' : { }}
+        CheckFind(query, 2)
+
+        query = { 'Level' : 'Study', 'Query' : { }}
+        CheckFind(query, 2)
+
+        query = { 'Level' : 'Series', 'Query' : { }}
+        CheckFind(query, 4)
+
+        query = { 'Level' : 'Instance', 'Query' : { }}
+        CheckFind(query, 12)
+
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '20061201-20061201' }}
+        a = CheckFind(query, 1)
         self.assertEqual('BRAINIX', a[0]['PatientMainDicomTags']['PatientName'])
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '20061201-20091201' }})
-        self.assertEqual(2, len(a))
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '20061201-20091201' }}
+        a = CheckFind(query, 2)
         for i in range(2):
             self.assertTrue(a[i]['PatientMainDicomTags']['PatientName'] in ['BRAINIX', 'KNEE'])
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Query' : { 'StudyDate' : '20061202-20061202' }})
-        self.assertEqual(0, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '-20061201' }})
-        self.assertEqual(1, len(a))
+        query = { 'Level' : 'Study',
+                  'Query' : { 'StudyDate' : '20061202-20061202' }}
+        CheckFind(query, 0)
+
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '-20061201' }}
+        a = CheckFind(query, 1)
         self.assertEqual('BRAINIX', a[0]['PatientMainDicomTags']['PatientName'])
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '-20051201' }})
-        self.assertEqual(0, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '20061201-' }})
-        self.assertEqual(2, len(a))
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '-20051201' }}
+        CheckFind(query, 0)
+
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '20061201-' }}
+        a = CheckFind(query, 2)
         for i in range(2):
             self.assertTrue(a[i]['PatientMainDicomTags']['PatientName'] in ['BRAINIX', 'KNEE'])
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '20061202-' }})
-        self.assertEqual(1, len(a))
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '20061202-' }}
+        a = CheckFind(query, 1)
         self.assertEqual('KNEE', a[0]['PatientMainDicomTags']['PatientName'])
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '20080819-' }})
-        self.assertEqual(1, len(a))
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '20080819-' }}
+        a = CheckFind(query, 1)
         self.assertEqual('KNEE', a[0]['PatientMainDicomTags']['PatientName'])
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Study',
-                                             'Expand' : True,
-                                             'Query' : { 'StudyDate' : '20080820-' }})
-        self.assertEqual(0, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Expand' : True,
-                                             'Query' : { 'PatientPosition' : 'HFS' }})
-        self.assertEqual(2, len(a))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Expand' : False,
-                                             'Query' : { 'PatientPosition' : 'HFS' }})
-        self.assertEqual(2, len(a))
+        query = { 'Level' : 'Study',
+                  'Expand' : True,
+                  'Query' : { 'StudyDate' : '20080820-' }}
+        CheckFind(query, 0)
+
+        query = { 'Level' : 'Series',
+                  'Expand' : True,
+                  'Query' : { 'PatientPosition' : 'HFS' }}
+        CheckFind(query, 2, False)   # "PatientPosition" is not a main DICOM tag, so unavailable in "/tools/count-resources"
+
+        query = { 'Level' : 'Series',
+                  'Expand' : False,
+                  'Query' : { 'PatientPosition' : 'HFS' }}
+        CheckFind(query, 2, False)   # "PatientPosition" is not a main DICOM tag, so unavailable in "/tools/count-resources"
         
 
     def test_rest_query_retrieve(self):
@@ -2529,21 +2655,21 @@
         self.assertEqual('887', i[i.keys()[0]]['PatientID'])
         self.assertEqual('887', i[i.keys()[1]]['PatientID'])
 
-        i = DoGet(_REMOTE, '/patients/%s/instances-tags?simplify' % DoGet(_REMOTE, '/studies')[0])
+        i = DoGet(_REMOTE, '/studies/%s/instances-tags?simplify' % DoGet(_REMOTE, '/studies')[0])
         self.assertEqual(2, len(i))
         self.assertEqual('887', i[i.keys()[0]]['PatientID'])
         self.assertEqual('887', i[i.keys()[1]]['PatientID'])
 
         self.assertEqual(2, len(DoGet(_REMOTE, '/series')))
-        i = DoGet(_REMOTE, '/patients/%s/instances-tags?simplify' % DoGet(_REMOTE, '/series')[0])
+        i = DoGet(_REMOTE, '/series/%s/instances-tags?simplify' % DoGet(_REMOTE, '/series')[0])
         self.assertEqual(1, len(i))
         self.assertEqual('887', i[i.keys()[0]]['PatientID'])
         
-        i = DoGet(_REMOTE, '/patients/%s/instances-tags?simplify' % DoGet(_REMOTE, '/series')[1])
+        i = DoGet(_REMOTE, '/series/%s/instances-tags?simplify' % DoGet(_REMOTE, '/series')[1])
         self.assertEqual(1, len(i))
         self.assertEqual('887', i[i.keys()[0]]['PatientID'])
 
-        i = DoGet(_REMOTE, '/patients/%s/instances-tags?short' % DoGet(_REMOTE, '/series')[1])
+        i = DoGet(_REMOTE, '/series/%s/instances-tags?short' % DoGet(_REMOTE, '/series')[1])
         self.assertEqual(1, len(i))
         self.assertEqual('887', i[i.keys()[0]]['0010,0020'])
 
@@ -3017,7 +3143,7 @@
         self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/patients&since=10' % i))
         self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/patients&limit=10' % i))
 
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5)  and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged:   # with ExtendedFind, the limit=0 means no-limit like in /tools/find
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5): # with ExtendedFind, the limit=0 means no-limit like in /tools/find
             self.assertEqual(2, len(DoGet(_REMOTE, '/patients?since=0&limit=0')))
             self.assertEqual(1, len(DoGet(_REMOTE, '/patients?since=1&limit=0')))
             self.assertEqual(0, len(DoGet(_REMOTE, '/patients?since=2&limit=0')))
@@ -4273,11 +4399,12 @@
                                              'Limit' : 4 })
         self.assertEqual(4, len(a))
 
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
-                                             'Query' : { 'PatientName' : 'B*' },
-                                             'Since' : 2,
-                                             'Limit' : 4 })
-        self.assertEqual(2, len(a))
+        if HasExtendedFind(_REMOTE):  # usage of since is not reliable without ExtendedFind
+            a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
+                                                'Query' : { 'PatientName' : 'B*' },
+                                                'Since' : 2,
+                                                'Limit' : 4 })
+            self.assertEqual(2, len(a))
 
         a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
                                              'Query' : { 'PatientName' : 'B*' },
@@ -4289,23 +4416,24 @@
                                              'Limit' : 0 })  # This is an arbitrary convention
         self.assertEqual(4, len(a))
 
-        b = []
-        for i in range(4):
+        if HasExtendedFind(_REMOTE):  # usage of since is not reliable without ExtendedFind
+            b = []
+            for i in range(4):
+                a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
+                                                    'Query' : { 'PatientName' : 'B*' },
+                                                    'Limit' : 1,
+                                                    'Since' : i })
+                self.assertEqual(1, len(a))
+                b.append(a[0])
+
+            # Check whether the two sets are equal through symmetric difference
+            self.assertEqual(0, len(set(b) ^ set(brainix)))
+
             a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
-                                                 'Query' : { 'PatientName' : 'B*' },
-                                                 'Limit' : 1,
-                                                 'Since' : i })
-            self.assertEqual(1, len(a))
-            b.append(a[0])
-
-        # Check whether the two sets are equal through symmetric difference
-        self.assertEqual(0, len(set(b) ^ set(brainix)))
-
-        a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
-                                             'Query' : { 'PatientName' : 'B*' },
-                                             'Limit' : 1,
-                                             'Since' : 4 })
-        self.assertEqual(0, len(a))
+                                                'Query' : { 'PatientName' : 'B*' },
+                                                'Limit' : 1,
+                                                'Since' : 4 })
+            self.assertEqual(0, len(a))
 
         # Check using KNEE
         a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
@@ -4318,109 +4446,114 @@
                                              'Limit' : 2 })
         self.assertEqual(2, len(a))
 
-        b = []
-        for i in range(2):
-            a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
-                                                 'Query' : { 'PatientName' : 'K*' },
-                                                 'Limit' : 1,
-                                                 'Since' : i })
-            self.assertEqual(1, len(a))
-            b.append(a[0])
-
-        self.assertEqual(0, len(set(b) ^ set(knee)))
+        if HasExtendedFind(_REMOTE):  # usage of since is not reliable without ExtendedFind
+            b = []
+            for i in range(2):
+                a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
+                                                    'Query' : { 'PatientName' : 'K*' },
+                                                    'Limit' : 1,
+                                                    'Since' : i })
+                self.assertEqual(1, len(a))
+                b.append(a[0])
+
+            self.assertEqual(0, len(set(b) ^ set(knee)))
 
         # Now test "isSimpleLookup_ == false"
         a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
                                              'Query' : { 'PatientPosition' : '*' }})
         self.assertEqual(3, len(a))
 
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Limit' : 0})
-        self.assertEqual(3, len(b))
-        self.assertEqual(a[0], b[0])
-        self.assertEqual(a[1], b[1])
-        self.assertEqual(a[2], b[2])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Limit' : 1})
-        self.assertEqual(1, len(b))
-        self.assertEqual(a[0], b[0])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 0,
-                                             'Limit' : 1})
-        self.assertEqual(1, len(b))
-        self.assertEqual(a[0], b[0])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 0,
-                                             'Limit' : 3})
-        self.assertEqual(3, len(b))
-        self.assertEqual(a[0], b[0])
-        self.assertEqual(a[1], b[1])
-        self.assertEqual(a[2], b[2])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 0,
-                                             'Limit' : 4})
-        self.assertEqual(3, len(b))
-        self.assertEqual(a[0], b[0])
-        self.assertEqual(a[1], b[1])
-        self.assertEqual(a[2], b[2])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 1,
-                                             'Limit' : 1})
-        self.assertEqual(1, len(b))
-        self.assertEqual(a[1], b[0])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 1,
-                                             'Limit' : 2})
-        self.assertEqual(2, len(b))
-        self.assertEqual(a[1], b[0])
-        self.assertEqual(a[2], b[1])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 1,
-                                             'Limit' : 3})
-        self.assertEqual(2, len(b))
-        self.assertEqual(a[1], b[0])
-        self.assertEqual(a[2], b[1])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 2,
-                                             'Limit' : 1})
-        self.assertEqual(1, len(b))
-        self.assertEqual(a[2], b[0])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 2,
-                                             'Limit' : 2})
-        self.assertEqual(1, len(b))
-        self.assertEqual(a[2], b[0])
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 3,
-                                             'Limit' : 1})
-        self.assertEqual(0, len(b))
-
-        b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                             'Query' : { 'PatientPosition' : '*' },
-                                             'Since' : 3,
-                                             'Limit' : 10})
-        self.assertEqual(0, len(b))
+        # TODO: remove these tests for good once 1.12.5 is out
+        # if not HasExtendedFind(_REMOTE):  # once you have ExtendedFind, usage of Limit and Since is forbidden when filtering on tags that are not in DB because that's just impossible to use on real life DB !
+
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Limit' : 0})
+        #     self.assertEqual(3, len(b))
+        #     self.assertEqual(a[0], b[0])
+        #     self.assertEqual(a[1], b[1])
+        #     self.assertEqual(a[2], b[2])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Limit' : 1})
+        #     self.assertEqual(1, len(b))
+        #     self.assertEqual(a[0], b[0])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 0,
+        #                                         'Limit' : 1})
+        #     self.assertEqual(1, len(b))
+        #     self.assertEqual(a[0], b[0])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 0,
+        #                                         'Limit' : 3})
+        #     self.assertEqual(3, len(b))
+        #     self.assertEqual(a[0], b[0])
+        #     self.assertEqual(a[1], b[1])
+        #     self.assertEqual(a[2], b[2])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 0,
+        #                                         'Limit' : 4})
+        #     self.assertEqual(3, len(b))
+        #     self.assertEqual(a[0], b[0])
+        #     self.assertEqual(a[1], b[1])
+        #     self.assertEqual(a[2], b[2])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 1,
+        #                                         'Limit' : 1})
+        #     self.assertEqual(1, len(b))
+        #     self.assertEqual(a[1], b[0])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 1,
+        #                                         'Limit' : 2})
+        #     self.assertEqual(2, len(b))
+        #     self.assertEqual(a[1], b[0])
+        #     self.assertEqual(a[2], b[1])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 1,
+        #                                         'Limit' : 3})
+        #     self.assertEqual(2, len(b))
+        #     self.assertEqual(a[1], b[0])
+        #     self.assertEqual(a[2], b[1])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 2,
+        #                                         'Limit' : 1})
+        #     self.assertEqual(1, len(b))
+        #     self.assertEqual(a[2], b[0])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 2,
+        #                                         'Limit' : 2})
+        #     self.assertEqual(1, len(b))
+        #     self.assertEqual(a[2], b[0])
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 3,
+        #                                         'Limit' : 1})
+        #     self.assertEqual(0, len(b))
+
+        #     b = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
+        #                                         'Query' : { 'PatientPosition' : '*' },
+        #                                         'Since' : 3,
+        #                                         'Limit' : 10})
+        #     self.assertEqual(0, len(b))
 
 
     def test_bitbucket_issue_46(self):
@@ -10247,8 +10380,18 @@
             Check('TransferSyntaxes/1.2.840.10008.1.2.1.dcm', True, False, 'OB') # Explicit Little Endian, 8bpp
             Check('Phenix/IM-0001-0001.dcm', True, False, 'OW')  # Explicit Little Endian, 16bpp
             Check('TransferSyntaxes/1.2.840.10008.1.2.2.dcm', True, False, 'OB') # Explicit Big Endian, 8bpp
-            Check('TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm', True, False, 'OB')  # JPEG
-            Check('Knee/T1/IM-0001-0001.dcm', True, False, 'OB') # JPEG2k
+            if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
+                # From Orthanc 1.12.6, the PixelData is not present.  Anyway, it was not usable in 1.12.5
+                Check('TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm', False, False, 'OB')  # JPEG
+                Check('Knee/T1/IM-0001-0001.dcm', False, False, 'OB') # JPEG2k
+            else:
+                # up to Orthanc 1.12.5, we get this (that is basically useless):
+                # "7FE00010" : {
+                #   "InlineBinary" : "",
+                #   "vr" : "OB"
+                # }
+                Check('TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm', True, False, 'OB')  # JPEG
+                Check('Knee/T1/IM-0001-0001.dcm', True, False, 'OB') # JPEG2k
 
     def test_encapsulate_stl(self):
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 1):
@@ -10720,7 +10863,7 @@
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 4):     # the old syntax is still required for the upgrade/downgrade PG tests
             a = DoGet(_REMOTE, '/instances/%s?requested-tags=0008,0056' % instance)
         else:
-            a = DoGet(_REMOTE, '/instances/%s?RequestedTags=0008,0056' % instance)
+            a = DoGet(_REMOTE, '/instances/%s?requestedTags=0008,0056' % instance)
         
         self.assertEqual(1, len(a['RequestedTags']))
         self.assertEqual('ONLINE', a['RequestedTags']['InstanceAvailability'])
@@ -10728,14 +10871,14 @@
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 4):
             a = DoGet(_REMOTE, '/series/%s?requested-tags=0020,1209' % series)
         else:
-            a = DoGet(_REMOTE, '/series/%s?RequestedTags=0020,1209' % series)
+            a = DoGet(_REMOTE, '/series/%s?requestedTags=0020,1209' % series)
         self.assertEqual(1, len(a['RequestedTags']))
         self.assertEqual(2, int(a['RequestedTags']['NumberOfSeriesRelatedInstances']))
 
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 4):
             a = DoGet(_REMOTE, '/studies/%s?requested-tags=0008,0061;0008,0062;0020,1206;0020,1208' % study)
         else:
-            a = DoGet(_REMOTE, '/studies/%s?RequestedTags=0008,0061;0008,0062;0020,1206;0020,1208' % study)
+            a = DoGet(_REMOTE, '/studies/%s?requestedTags=0008,0061;0008,0062;0020,1206;0020,1208' % study)
 
         self.assertEqual(4, len(a['RequestedTags']))
         self.assertEqual('CT\\PT', a['RequestedTags']['ModalitiesInStudy'])
@@ -10746,7 +10889,7 @@
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 4):
             a = DoGet(_REMOTE, '/patients/%s?requested-tags=0020,1200;0020,1202;0020,1204' % patient)
         else:
-            a = DoGet(_REMOTE, '/studies/%s?RequestedTags=0020,1200;0020,1202;0020,1204' % study)
+            a = DoGet(_REMOTE, '/studies/%s?requestedTags=0020,1200;0020,1202;0020,1204' % study)
         self.assertEqual(3, len(a['RequestedTags']))
         self.assertEqual(1, int(a['RequestedTags']['NumberOfPatientRelatedStudies']))
         self.assertEqual(2, int(a['RequestedTags']['NumberOfPatientRelatedSeries']))
@@ -10787,14 +10930,17 @@
 
 
     def test_extended_find_order_by(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged
-
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
             # Upload 12 instances
             for i in range(3):
-                UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-000%d.dcm' % (i + 1))
-                UploadInstance(_REMOTE, 'Brainix/Epi/IM-0001-000%d.dcm' % (i + 1))
-                UploadInstance(_REMOTE, 'Knee/T1/IM-0001-000%d.dcm' % (i + 1))
-                UploadInstance(_REMOTE, 'Knee/T2/IM-0001-000%d.dcm' % (i + 1))
+                r = UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-000%d.dcm' % (i + 1))
+                DoPut(_REMOTE, '/instances/%s/metadata/1234' % r['ID'], '%f' % (10.0 + 0.1 * i))
+                r = UploadInstance(_REMOTE, 'Brainix/Epi/IM-0001-000%d.dcm' % (i + 1))
+                DoPut(_REMOTE, '/instances/%s/metadata/1234' % r['ID'], '%f' % (20.0 + 0.1 * i))
+                r = UploadInstance(_REMOTE, 'Knee/T1/IM-0001-000%d.dcm' % (i + 1))
+                DoPut(_REMOTE, '/instances/%s/metadata/1234' % r['ID'], '%f' % (30.0 + 0.1 * i))
+                r = UploadInstance(_REMOTE, 'Knee/T2/IM-0001-000%d.dcm' % (i + 1))
+                DoPut(_REMOTE, '/instances/%s/metadata/1234' % r['ID'], '%f' % (40.0 + 0.1 * i))
 
             kneeT2SeriesId = 'bbf7a453-0d34251a-03663b55-46bb31b9-ffd74c59'
             kneeT1SeriesId = '6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285'
@@ -10924,7 +11070,7 @@
                                                         'Direction': 'ASC'
                                                     },
                                                     {
-                                                        'Type': 'DicomTag',
+                                                        'Type': 'DicomTagAsInt',
                                                         'Key': 'InstanceNumber',
                                                         'Direction': 'ASC'
                                                     },
@@ -10940,7 +11086,7 @@
             for i in range(1, len(a)-1):
                 self.assertTrue(a[i-1]['RequestedTags']['PatientBirthDate'] <= a[i]['RequestedTags']['PatientBirthDate'])
                 if a[i-1]['RequestedTags']['PatientBirthDate'] == a[i]['RequestedTags']['PatientBirthDate']:
-                    self.assertTrue(a[i-1]['RequestedTags']['InstanceNumber'] <= a[i]['RequestedTags']['InstanceNumber'])
+                    self.assertTrue(int(a[i-1]['RequestedTags']['InstanceNumber']) <= int(a[i]['RequestedTags']['InstanceNumber']))
                     if a[i-1]['RequestedTags']['InstanceNumber'] == a[i]['RequestedTags']['InstanceNumber']:
                         self.assertTrue(a[i-1]['RequestedTags']['SeriesTime'] <= a[i]['RequestedTags']['SeriesTime'])    
 
@@ -10951,7 +11097,7 @@
                                                 },
                                                 'OrderBy' : [
                                                     {
-                                                        'Type': 'DicomTag',
+                                                        'Type': 'DicomTagAsInt',
                                                         'Key': 'InstanceNumber',
                                                         'Direction': 'DESC'
                                                     },
@@ -10970,7 +11116,7 @@
                                                 })
             self.assertEqual(12, len(a))
             for i in range(1, len(a)-1):
-                self.assertTrue(a[i-1]['RequestedTags']['InstanceNumber'] >= a[i]['RequestedTags']['InstanceNumber'])
+                self.assertTrue(int(a[i-1]['RequestedTags']['InstanceNumber']) >= int(a[i]['RequestedTags']['InstanceNumber']))
                 if a[i-1]['RequestedTags']['InstanceNumber'] == a[i]['RequestedTags']['InstanceNumber']:
                     self.assertTrue(a[i-1]['RequestedTags']['PatientBirthDate'] <= a[i]['RequestedTags']['PatientBirthDate'])
                     if a[i-1]['RequestedTags']['PatientBirthDate'] == a[i]['RequestedTags']['PatientBirthDate']:
@@ -11052,8 +11198,37 @@
             self.assertEqual(kneeT1SeriesId, a[3])
 
 
+            a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Instance',
+                                                'ResponseContent': ['Metadata', 'RequestedTags'],
+                                                'Query' : { 
+                                                },
+                                                'OrderBy' : [
+                                                    {
+                                                        'Type': 'MetadataAsFloat',
+                                                        'Key': '1234',
+                                                        'Direction': 'DESC'
+                                                    }
+                                                ],
+                                                'RequestedTags' : ['SeriesDescription']
+                                                })
+            self.assertEqual(12, len(a))
+            for i in range(0, 2):
+                self.assertEqual("T2W_TSE", a[i]['RequestedTags']['SeriesDescription'])
+            self.assertAlmostEqual(40.2, float(a[0]['Metadata']['1234']))
+            self.assertAlmostEqual(40.0, float(a[2]['Metadata']['1234']))
+
+            for i in range(3, 5):
+                self.assertEqual("T1W_aTSE", a[i]['RequestedTags']['SeriesDescription'])
+
+            for i in range(6, 8):
+                self.assertEqual("T2W/FE-EPI", a[i]['RequestedTags']['SeriesDescription'])
+
+            for i in range(9, 11):
+                self.assertEqual("sT2W/FLAIR", a[i]['RequestedTags']['SeriesDescription'])
+
+
     def test_extended_find_parent(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
             # Upload 12 instances
             for i in range(3):
                 UploadInstance(_REMOTE, 'Knee/T1/IM-0001-000%d.dcm' % (i + 1))
@@ -11098,10 +11273,19 @@
 
             self.assertEqual(6, len(a))
 
+            # same query in count-resources
+            a = DoPost(_REMOTE, '/tools/count-resources', { 'Level' : 'Instance',
+                                                 'Query' : { 
+                                                    'SeriesDescription' : 'T*'
+                                                },
+                                                'ParentPatient' : kneePatientId
+                                                })
+
+            self.assertEqual(6, a["Count"])
+
 
     def test_extended_find_filter_metadata(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged
-
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
             # Upload 12 instances
             for i in range(3):
                 UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-000%d.dcm' % (i + 1))
@@ -11119,20 +11303,27 @@
             DoPut(_REMOTE, '/series/%s/metadata/my-metadata' % brainixEpiSeriesId, 'brainixEpi')
 
             # filter on metadata
-            a = DoPost(_REMOTE, '/tools/find', { 'Level' : 'Series',
-                                                 'Query' : { 
-                                                    'SeriesDescription' : 'T*'
-                                                },
-                                                'QueryMetadata' : {
-                                                    'my-metadata': '*2*'
-                                                }
-                                                })
+            q = {
+                'Level' : 'Series',
+                'Query' : { 
+                    'SeriesDescription' : 'T*'
+                },
+                'MetadataQuery' : {
+                    'my-metadata': '*2*'
+                }
+            }
+            a = DoPost(_REMOTE, '/tools/find', q)
 
             self.assertEqual(1, len(a))
             self.assertEqual(kneeT2SeriesId, a[0])
 
+            a = DoPost(_REMOTE, '/tools/count-resources', q)
+            self.assertEqual(1, a["Count"])
+
+
+
     def test_extended_find_expand(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5):
             UploadInstance(_REMOTE, 'Knee/T2/IM-0001-0001.dcm')
 
             a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Series',
@@ -11256,8 +11447,7 @@
 
 
     def test_extended_find_full(self):
-        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged
-
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
             # Upload 12 instances
             for i in range(3):
                 UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-000%d.dcm' % (i + 1))
@@ -11281,7 +11471,7 @@
                                                         'PatientName' : '*'
                                                     },
                                                     'RequestedTags': ['StudyDate'],
-                                                    'QueryMetadata' : {
+                                                    'MetadataQuery' : {
                                                         'my-metadata': "*nee*"
                                                     },
                                                     'OrderBy' : [
@@ -11305,4 +11495,275 @@
             self.assertEqual(kneeT2SeriesId, a[1]['ID'])
             self.assertEqual(kneeStudyId, a[0]['ParentStudy'])
             self.assertEqual(3, len(a[0]['Instances']))
-            self.assertEqual('', a[0]['Metadata']['RemoteAET'])
\ No newline at end of file
+            self.assertEqual('', a[0]['Metadata']['RemoteAET'])
+
+    def test_pagination_and_limit_find_results(self):
+        # LimitFindInstances is set to 20
+        # LimitFindResults is set to 10
+
+        # Upload 27 instances from KNIX
+        UploadFolder(_REMOTE, 'Knix/Loc')
+
+        # Upload 13 other series
+        UploadInstance(_REMOTE, 'DummyCT.dcm')
+        UploadInstance(_REMOTE, 'Phenix/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Implicit-vr-us-palette.dcm')
+        UploadInstance(_REMOTE, 'Multiframe.dcm')
+        UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Knee/T1/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Knee/T2/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'PrivateTags.dcm')
+        UploadInstance(_REMOTE, 'PrivateMDNTags.dcm')
+        UploadInstance(_REMOTE, 'Comunix/Ct/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Comunix/Pet/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Beaufix/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Encodings/Lena-ascii.dcm')
+
+        self.assertEqual(14, len(DoGet(_REMOTE, '/series')))
+
+
+        # knixInstancesNoLimit = DoPost(_REMOTE, '/tools/find', {    
+        #                                         'Level' : 'Instances',
+        #                                         'Query' : { 
+        #                                             'PatientName' : 'KNIX'
+        #                                         },
+        #                                         'Expand': False
+        #                                         })
+
+        # # pprint.pprint(knixInstancesNoLimit)
+        # if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
+        #     self.assertEqual(20, len(knixInstancesNoLimit))
+        # else:
+        #     self.assertEqual(21, len(knixInstancesNoLimit))
+
+        # knixInstancesSince5Limit20 = DoPost(_REMOTE, '/tools/find', {    
+        #                                         'Level' : 'Instances',
+        #                                         'Query' : { 
+        #                                             'PatientName' : 'KNIX'
+        #                                         },
+        #                                         'Expand': False,
+        #                                         'Since': 5,
+        #                                         'Limit': 20
+        #                                         })
+        # # pprint.pprint(knixInstancesSince5Limit20)
+        
+        # if IsOrthancVersionAbove(_REMOTE, 1, 12, 5):
+        #     self.assertEqual(20, len(knixInstancesSince5Limit20))  # Orthanc actually returns LimitFindInstances + 1 resources
+        #     # the first 5 from previous call shall not be in this answer
+        #     for i in range(0, 5):
+        #         self.assertNotIn(knixInstancesNoLimit[i], knixInstancesSince5Limit20)
+        #     # the last 4 from last call shall not be in the first answer
+        #     for i in range(16, 20):
+        #         self.assertNotIn(knixInstancesSince5Limit20[i], knixInstancesNoLimit)
+
+        # # request more instances than LimitFindInstances
+        # knixInstancesSince0Limit23 = DoPost(_REMOTE, '/tools/find', {    
+        #                                         'Level' : 'Instances',
+        #                                         'Query' : { 
+        #                                             'PatientName' : 'KNIX'
+        #                                         },
+        #                                         'Expand': False,
+        #                                         'Since': 0,
+        #                                         'Limit': 23
+        #                                         })
+        # if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
+        #     self.assertEqual(20, len(knixInstancesSince0Limit23))
+
+        # seriesNoLimit = DoPost(_REMOTE, '/tools/find', {    
+        #                                         'Level' : 'Series',
+        #                                         'Query' : { 
+        #                                             'PatientName' : '*'
+        #                                         },
+        #                                         'Expand': False
+        #                                         })
+
+        # # pprint.pprint(seriesNoLimit)
+        # if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
+        #     self.assertEqual(10, len(seriesNoLimit))
+        # else:
+        #     self.assertEqual(11, len(seriesNoLimit))
+
+        # seriesSince8Limit6 = DoPost(_REMOTE, '/tools/find', {    
+        #                                         'Level' : 'Series',
+        #                                         'Query' : { 
+        #                                             'PatientName' : '*'
+        #                                         },
+        #                                         'Expand': False,
+        #                                         'Since': 8,
+        #                                         'Limit': 6
+        #                                         })
+
+        # # pprint.pprint(seriesSince8Limit6)
+        # if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE): # TODO: remove HasExtendedFind once find-refactoring branch has been merged and supported by all DB plugins !!!
+        #     self.assertEqual(6, len(seriesSince8Limit6))
+
+        #     # the first 7 from previous call shall not be in this answer
+        #     for i in range(0, 7):
+        #         self.assertNotIn(seriesNoLimit[i], seriesSince8Limit6)
+        #     # the last 3 from last call shall not be in the first answer
+        #     for i in range(3, 5):
+        #         self.assertNotIn(seriesSince8Limit6[i], seriesNoLimit)
+
+        # if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
+        #     # query by a tag that is not in the DB (there are 27 instances from Knix/Loc + 10 instances from other series that satisfies this criteria)
+        #     a = DoPost(_REMOTE, '/tools/find', {    
+        #                                             'Level' : 'Instances',
+        #                                             'Query' : { 
+        #                                                 'PhotometricInterpretation' : 'MONOCHROME*'
+        #                                             },
+        #                                             'Expand': True,
+        #                                             'OrderBy' : [
+        #                                                     {
+        #                                                         'Type': 'DicomTag',
+        #                                                         'Key': 'InstanceNumber',
+        #                                                         'Direction': 'ASC'
+        #                                                     }
+        #                                             ]})
+
+        #     # pprint.pprint(a)
+        #     # print(len(a))
+        #     # TODO: we should have something in the response that notifies us that the response is not "complete"
+        #     # TODO: we should receive an error if we try to use "since" in this kind of search ?
+        #     self.assertEqual(17, len(a))   # the fast DB filtering returns 20 instances -> only 17 of them meet the criteria but this is not really correct !!!
+
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):
+            # make sur an error is returned when using Since or Limit when querying a tag that is not in DB
+            self.assertRaises(Exception, lambda: DoPost(_REMOTE, '/tools/find', {'Level' : 'Instances',
+                                                    'Query' : { 
+                                                        'PhotometricInterpretation' : 'MONOCHROME*'
+                                                    },
+                                                    'Since': 2
+                                                    }))
+
+            # make sur an error is returned when using Since when querying a tag that is not in DB
+            self.assertRaises(Exception, lambda: DoPost(_REMOTE, '/tools/find', {'Level' : 'Instances',
+                                                    'Query' : { 
+                                                        'PhotometricInterpretation' : 'MONOCHROME*'
+                                                    },
+                                                    'Since': 10
+                                                    }))
+
+        # https://github.com/orthanc-server/orthanc-explorer-2/issues/73
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 6) and HasExtendedFind(_REMOTE):
+            # make sur no error is returned when using Since or Limit when querying against ModalitiesInStudy
+            a = DoPost(_REMOTE, '/tools/find', {'Level' : 'Studies',
+                                                'Query' : { 
+                                                    'ModalitiesInStudy' : 'CT\\MR'
+                                                },
+                                                'Since': 2,
+                                                'Limit': 3,
+                                                'Expand': True,
+                                                'OrderBy': [
+                                                    {
+                                                        'Type': 'DicomTag',
+                                                        'Key': 'StudyDate',
+                                                        'Direction': 'ASC'
+                                                    }                                                        
+                                                ]})
+            # pprint.pprint(a)
+            self.assertEqual('20050927', a[0]['MainDicomTags']['StudyDate'])
+            self.assertEqual('20061201', a[1]['MainDicomTags']['StudyDate'])
+            self.assertEqual('20070101', a[2]['MainDicomTags']['StudyDate'])
+
+
+    def test_attachment_range(self):
+        def TestData(path):
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/%s' % (i, path))
+            self.assertFalse('content-range' in resp)
+            self.assertEqual(200, resp.status)
+            self.assertEqual(2472, len(content))
+            self.assertEqual('2472', resp['content-length'])
+            self.assertEqual('application/dicom', resp['content-type'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/%s' % (i, path), headers = { 'Range' : 'bytes=128-131' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(4, len(content))
+            self.assertEqual('D', content[0])
+            self.assertEqual('I', content[1])
+            self.assertEqual('C', content[2])
+            self.assertEqual('M', content[3])
+            self.assertEqual('4', resp['content-length'])
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 128-131/2472', resp['content-range'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/%s' % (i, path), headers = { 'Range' : 'bytes=-' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(2472, len(content))
+            self.assertEqual('2472', resp['content-length'])
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 0-2471/2472', resp['content-range'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/%s' % (i, path), headers = { 'Range' : 'bytes=128-' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(2344, len(content))
+            self.assertEqual('D', content[0])
+            self.assertEqual('I', content[1])
+            self.assertEqual('C', content[2])
+            self.assertEqual('M', content[3])
+            self.assertEqual('2344', resp['content-length'])
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 128-2471/2472', resp['content-range'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/%s' % (i, path), headers = { 'Range' : 'bytes=-131' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(132, len(content))
+            self.assertEqual('D', content[-4])
+            self.assertEqual('I', content[-3])
+            self.assertEqual('C', content[-2])
+            self.assertEqual('M', content[-1])
+            self.assertEqual('132', resp['content-length'])
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 0-131/2472', resp['content-range'])
+
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 5):
+            i = UploadInstance(_REMOTE, 'DummyCT.dcm') ['ID']
+
+            DoPost(_REMOTE, '/instances/%s/attachments/dicom/uncompress' % i)
+            TestData('data')
+            TestData('compressed-data')
+
+            DoPost(_REMOTE, '/instances/%s/attachments/dicom/compress' % i)
+            TestData('data')
+
+            (resp, compressed) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/compressed-data' % i)
+            self.assertFalse('content-range' in resp)
+            self.assertEqual(200, resp.status)
+            self.assertTrue(len(compressed) < 2000)
+            self.assertEqual(len(compressed), int(resp['content-length']))
+            self.assertEqual('application/octet-stream', resp['content-type'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/compressed-data' % i, headers = { 'Range' : 'bytes=-' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(compressed, content)
+            self.assertEqual(len(compressed), int(resp['content-length']))
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 0-%d/%d' % (len(compressed) - 1, len(compressed)), resp['content-range'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/compressed-data' % i, headers = { 'Range' : 'bytes=10-' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(compressed[10:], content)
+            self.assertEqual(len(compressed) - 10, int(resp['content-length']))
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 10-%d/%d' % (len(compressed) - 1, len(compressed)), resp['content-range'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/compressed-data' % i, headers = { 'Range' : 'bytes=-20' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(compressed[0:21], content)
+            self.assertEqual(21, int(resp['content-length']))
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 0-20/%d' % len(compressed), resp['content-range'])
+
+            (resp, content) = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/compressed-data' % i, headers = { 'Range' : 'bytes=10-20' })
+            self.assertTrue('content-range' in resp)
+            self.assertEqual(206, resp.status)
+            self.assertEqual(compressed[10:21], content)
+            self.assertEqual(11, int(resp['content-length']))
+            self.assertEqual('application/octet-stream', resp['content-type'])
+            self.assertEqual('bytes 10-20/%d' % len(compressed), resp['content-range'])
--- a/Tests/Toolbox.py	Wed Oct 09 11:07:09 2024 +0200
+++ b/Tests/Toolbox.py	Thu Jan 30 17:38:39 2025 +0100
@@ -4,8 +4,8 @@
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 # Department, University Hospital of Liege, Belgium
 # Copyright (C) 2017-2023 Osimis S.A., Belgium
-# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
-# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -394,7 +394,7 @@
     return count
 
 
-def IsPluginVersionAbove(orthanc, plugin, major, minor, revision):
+def IsPluginVersionAtLeast(orthanc, plugin, major, minor, revision):
     v = DoGet(orthanc, '/plugins/%s' % plugin)['Version']
 
     if v.startswith('mainline'):
@@ -412,7 +412,7 @@
             a = int(tmp[0])
             b = int(tmp[1])
             return (a > major or
-                    (a == major and b > minor))
+                    (a == major and b >= minor))
         else:
             return False