diff --git a/adit/core/templates/core/admin_section.html b/adit/core/templates/core/admin_section.html
index ec7b4fc8f..201a90c72 100644
--- a/adit/core/templates/core/admin_section.html
+++ b/adit/core/templates/core/admin_section.html
@@ -28,6 +28,27 @@
Job Overview
{% endfor %}
+ API Usage
+
+
+
+ | User |
+ Time of last request |
+ Total Response Size |
+ Total number of requests |
+
+
+
+ {% for session in api_stats %}
+
+ | {{ session.owner }} |
+ {{ session.time_last_accessed }} |
+ {{ session.total_transfer_size | filesizeformat }} |
+ {{ session.total_number_requests }} |
+
+ {% endfor %}
+
+
Admin Tools
-
diff --git a/adit/core/views.py b/adit/core/views.py
index 41bc32b72..7791d35a9 100644
--- a/adit/core/views.py
+++ b/adit/core/views.py
@@ -31,6 +31,7 @@
from procrastinate.contrib.django import app
from adit.core.utils.model_utils import reset_tasks
+from adit.dicom_web.apps import collect_latest_api_usage
from .models import DicomJob, DicomTask
from .site import job_stats_collectors
@@ -46,13 +47,11 @@ def health(request: HttpRequest) -> JsonResponse:
def admin_section(request: HttpRequest) -> HttpResponse:
status_list = DicomJob.Status.choices
job_stats = [collector() for collector in job_stats_collectors]
+ api_stats = collect_latest_api_usage()
return render(
request,
"core/admin_section.html",
- {
- "status_list": status_list,
- "job_stats": job_stats,
- },
+ {"status_list": status_list, "job_stats": job_stats, "api_stats": api_stats},
)
diff --git a/adit/dicom_web/admin.py b/adit/dicom_web/admin.py
index 27fe94a66..59fcc09c2 100644
--- a/adit/dicom_web/admin.py
+++ b/adit/dicom_web/admin.py
@@ -1,5 +1,35 @@
from django.contrib import admin
-from .models import DicomWebSettings
+from .models import APIUsage, DicomWebSettings
admin.site.register(DicomWebSettings, admin.ModelAdmin)
+
+
+class APIUsageAdmin(admin.ModelAdmin):
+ list_display = (
+ "get_owner",
+ "id",
+ "time_last_accessed",
+ "total_number_requests",
+ "get_size",
+ )
+ list_filter = ("total_number_requests", "owner")
+ search_fields = ("owner__username",)
+
+ def get_owner(self, obj):
+ return obj.owner.username
+
+ get_owner.admin_order_field = "owner__username"
+ get_owner.short_description = "Owner"
+
+ def get_size(self, obj):
+ size = obj.total_transfer_size
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
+ if size < 1024:
+ return f"{size:.2f} {unit}"
+ size /= 1024
+
+ get_size.short_description = "Total Transfer Size"
+
+
+admin.site.register(APIUsage, APIUsageAdmin)
diff --git a/adit/dicom_web/apps.py b/adit/dicom_web/apps.py
index edb56e9f5..d207e5b4b 100644
--- a/adit/dicom_web/apps.py
+++ b/adit/dicom_web/apps.py
@@ -14,3 +14,9 @@ def init_db(**kwargs):
if not DicomWebSettings.objects.exists():
DicomWebSettings.objects.create()
+
+
+def collect_latest_api_usage():
+ from .models import APIUsage
+
+ return APIUsage.objects.order_by("-time_last_accessed")[:3]
diff --git a/adit/dicom_web/migrations/0004_apiusage.py b/adit/dicom_web/migrations/0004_apiusage.py
new file mode 100644
index 000000000..ba035bb63
--- /dev/null
+++ b/adit/dicom_web/migrations/0004_apiusage.py
@@ -0,0 +1,58 @@
+# Generated by Django 5.2.8 on 2025-12-08 20:58
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("dicom_web", "0003_add_dicom_web_permissions"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="APIUsage",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "time_last_accessed",
+ models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ ("total_transfer_size", models.BigIntegerField(default=0)),
+ ("total_number_requests", models.IntegerField(default=0)),
+ (
+ "owner",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="api_usage",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ options={
+ "indexes": [
+ models.Index(
+ fields=["-time_last_accessed"],
+ name="dicom_web_a_time_la_866621_idx",
+ )
+ ],
+ "constraints": [
+ models.UniqueConstraint(
+ fields=("owner",), name="unique_owner_api_usage"
+ )
+ ],
+ },
+ ),
+ ]
diff --git a/adit/dicom_web/models.py b/adit/dicom_web/models.py
index 0b74f47ba..c18d03fda 100644
--- a/adit/dicom_web/models.py
+++ b/adit/dicom_web/models.py
@@ -1,3 +1,7 @@
+from django.conf import settings
+from django.db import models
+from django.utils import timezone
+
from adit.core.models import DicomAppSettings
@@ -9,3 +13,23 @@ class Meta:
("can_retrieve", "Can retrieve"),
("can_store", "Can store"),
]
+
+
+class APIUsage(models.Model):
+ time_last_accessed = models.DateTimeField(default=timezone.now)
+ total_transfer_size = models.BigIntegerField(default=0)
+ total_number_requests = models.IntegerField(default=0)
+ owner = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ related_name="api_usage",
+ )
+
+ class Meta:
+ indexes = [
+ models.Index(fields=["-time_last_accessed"]),
+ ]
+ constraints = [models.UniqueConstraint(fields=["owner"], name="unique_owner_api_usage")]
+
+ def __str__(self) -> str:
+ return f"{self.__class__.__name__} [{self.pk}]"
diff --git a/adit/dicom_web/views.py b/adit/dicom_web/views.py
index e519b5dbf..ca9f43860 100644
--- a/adit/dicom_web/views.py
+++ b/adit/dicom_web/views.py
@@ -1,13 +1,16 @@
+import json
import logging
+from contextlib import asynccontextmanager
from typing import AsyncIterator, Literal, Union, cast
from adit_radis_shared.common.types import AuthenticatedApiRequest, User
from adrf.views import APIView as AsyncApiView
from channels.db import database_sync_to_async
from django.core.exceptions import ValidationError as DRFValidationError
-from django.db.models import QuerySet
+from django.db.models import F, QuerySet
from django.http import StreamingHttpResponse
from django.urls import reverse
+from django.utils import timezone
from pydicom import Dataset, Sequence
from rest_framework.exceptions import NotFound, ParseError, UnsupportedMediaType, ValidationError
from rest_framework.response import Response
@@ -15,6 +18,7 @@
from adit.core.models import DicomServer
from adit.core.utils.dicom_dataset import QueryDataset
+from adit.dicom_web.models import APIUsage
from adit.dicom_web.utils.peekable import AsyncPeekable
from adit.dicom_web.validators import (
validate_pseudonym,
@@ -60,13 +64,39 @@ async def _get_dicom_server(
f'Server with AE title "{ae_title}" does not exist or is not accessible.'
)
+ @database_sync_to_async
+ def _create_statistic(self, user: User) -> APIUsage:
+ statistic, _ = APIUsage.objects.get_or_create(owner=user)
+ return statistic
+
+ @database_sync_to_async
+ def _finalize_statistic(self, statistic: APIUsage, transfer_size: int = 0) -> None:
+ if transfer_size is not None:
+ APIUsage.objects.filter(pk=statistic.pk).update(
+ total_transfer_size=F("total_transfer_size") + transfer_size,
+ total_number_requests=F("total_number_requests") + 1,
+ time_last_accessed=timezone.now(),
+ )
+
+ @asynccontextmanager
+ async def track_session(self, user: User):
+ """Context manager to track API session for the duration of a request."""
+ session = await self._create_statistic(user)
+ try:
+ yield session
+ finally:
+ pass
+
# TODO: respect permission can_query
class QueryAPIView(WebDicomAPIView):
renderer_classes = [QidoApplicationDicomJsonRenderer]
def _extract_query_parameters(
- self, request: AuthenticatedApiRequest, study_uid: str | None = None
+ self,
+ request: AuthenticatedApiRequest,
+ study_uid: str | None = None,
+ series_uid: str | None = None,
) -> tuple[QueryDataset, Union[int, None]]:
"""
Filters a valid DICOMweb requests GET parameters and returns a QueryDataset and a
@@ -83,6 +113,9 @@ def _extract_query_parameters(
if study_uid is not None:
query["StudyInstanceUID"] = study_uid
+ if series_uid is not None:
+ query["SeriesInstanceUID"] = series_uid
+
limit_results = None
if "limit" in query:
limit_results = int(query.pop("limit"))
@@ -98,48 +131,63 @@ async def get(
request: AuthenticatedApiRequest,
ae_title: str,
) -> Response:
- query_ds, limit_results = self._extract_query_parameters(request)
- source_server = await self._get_dicom_server(request, ae_title, "source")
-
- try:
- results = await qido_find(source_server, query_ds, limit_results, "STUDY")
- except ValueError as err:
- logger.warning(f"Invalid DICOMweb study query - {err}")
- raise ValidationError(str(err)) from err
+ async with self.track_session(request.user) as session:
+ query_ds, limit_results = self._extract_query_parameters(request)
+ source_server = await self._get_dicom_server(request, ae_title, "source")
- return Response([result.dataset.to_json_dict() for result in results])
+ try:
+ results = await qido_find(source_server, query_ds, limit_results, "STUDY")
+ except ValueError as err:
+ logger.warning(f"Invalid DICOMweb study query - {err}")
+ raise ValidationError(str(err)) from err
+
+ response_data = [result.dataset.to_json_dict() for result in results]
+ await self._finalize_statistic(
+ session, transfer_size=len(json.dumps(response_data).encode())
+ )
+ return Response(response_data)
class QuerySeriesAPIView(QueryAPIView):
async def get(
self, request: AuthenticatedApiRequest, ae_title: str, study_uid: str
) -> Response:
- query_ds, limit_results = self._extract_query_parameters(request, study_uid)
- source_server = await self._get_dicom_server(request, ae_title, "source")
+ async with self.track_session(request.user) as session:
+ query_ds, limit_results = self._extract_query_parameters(request, study_uid)
+ source_server = await self._get_dicom_server(request, ae_title, "source")
- try:
- results = await qido_find(source_server, query_ds, limit_results, "SERIES")
- except ValueError as err:
- logger.warning(f"Invalid DICOMweb series query - {err}")
- raise ValidationError(str(err)) from err
-
- return Response([result.dataset.to_json_dict() for result in results])
+ try:
+ results = await qido_find(source_server, query_ds, limit_results, "SERIES")
+ except ValueError as err:
+ logger.warning(f"Invalid DICOMweb series query - {err}")
+ raise ValidationError(str(err)) from err
+
+ response_data = [result.dataset.to_json_dict() for result in results]
+ await self._finalize_statistic(
+ session, transfer_size=len(json.dumps(response_data).encode())
+ )
+ return Response(response_data)
class QueryImagesAPIView(QueryAPIView):
async def get(
- self, request: AuthenticatedApiRequest, ae_title: str, series_uid: str
+ self, request: AuthenticatedApiRequest, ae_title: str, study_uid: str, series_uid: str
) -> Response:
- query_ds, limit_results = self._extract_query_parameters(request, series_uid)
- source_server = await self._get_dicom_server(request, ae_title, "source")
+ async with self.track_session(request.user) as session:
+ query_ds, limit_results = self._extract_query_parameters(request, study_uid, series_uid)
+ source_server = await self._get_dicom_server(request, ae_title, "source")
- try:
- results = await qido_find(source_server, query_ds, limit_results, "IMAGE")
- except ValueError as err:
- logger.warning(f"Invalid DICOMweb image query - {err}")
- raise ValidationError(str(err)) from err
-
- return Response([result.dataset.to_json_dict() for result in results])
+ try:
+ results = await qido_find(source_server, query_ds, limit_results, "IMAGE")
+ except ValueError as err:
+ logger.warning(f"Invalid DICOMweb image query - {err}")
+ raise ValidationError(str(err)) from err
+
+ response_data = [result.dataset.to_json_dict() for result in results]
+ await self._finalize_statistic(
+ session, transfer_size=len(json.dumps(response_data).encode())
+ )
+ return Response(response_data)
# TODO: respect permission can_retrieve
@@ -225,116 +273,152 @@ def _get_trial_protocol_name(self, request: AuthenticatedApiRequest) -> str | No
return trial_protocol_name
+class _StreamingSessionWrapper:
+ """Wraps streaming content to track transfer size asynchronously."""
+
+ def __init__(self, renderer_generator, session: APIUsage, finalize_func):
+ self.renderer_generator = renderer_generator
+ self.session = session
+ self.finalize_func = finalize_func
+ self.total_size = 0
+
+ async def __aiter__(self):
+ try:
+ async for chunk in self.renderer_generator:
+ self.total_size += len(chunk) if isinstance(chunk, bytes) else len(chunk.encode())
+ yield chunk
+ finally:
+ await self.finalize_func(self.session, transfer_size=self.total_size)
+
+
class RetrieveStudyAPIView(RetrieveAPIView):
async def get(
self, request: AuthenticatedApiRequest, ae_title: str, study_uid: str
) -> StreamingHttpResponse:
- source_server = await self._get_dicom_server(request, ae_title)
- pseudonym = self._get_pseudonym(request)
- trial_protocol_id = self._get_trial_protocol_id(request)
- trial_protocol_name = self._get_trial_protocol_name(request)
-
- query = self.query.copy()
- query["StudyInstanceUID"] = study_uid
-
- images = wado_retrieve(
- source_server,
- query,
- "STUDY",
- pseudonym=pseudonym,
- trial_protocol_id=trial_protocol_id,
- trial_protocol_name=trial_protocol_name,
- )
- images = await self.peek_images(images)
-
- renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
- return StreamingHttpResponse(
- streaming_content=renderer.render(images),
- content_type=renderer.content_type,
- )
+ async with self.track_session(request.user) as session:
+ source_server = await self._get_dicom_server(request, ae_title)
+ pseudonym = self._get_pseudonym(request)
+ trial_protocol_id = self._get_trial_protocol_id(request)
+ trial_protocol_name = self._get_trial_protocol_name(request)
+
+ query = self.query.copy()
+ query["StudyInstanceUID"] = study_uid
+
+ images = wado_retrieve(
+ source_server,
+ query,
+ "STUDY",
+ pseudonym=pseudonym,
+ trial_protocol_id=trial_protocol_id,
+ trial_protocol_name=trial_protocol_name,
+ )
+ images = await self.peek_images(images)
+
+ renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
+ return StreamingHttpResponse(
+ streaming_content=_StreamingSessionWrapper(
+ renderer.render(images), session, self._finalize_statistic
+ ),
+ content_type=renderer.content_type,
+ )
class RetrieveStudyMetadataAPIView(RetrieveStudyAPIView):
async def get(
self, request: AuthenticatedApiRequest, ae_title: str, study_uid: str
) -> Response:
- source_server = await self._get_dicom_server(request, ae_title)
- pseudonym = self._get_pseudonym(request)
- trial_protocol_id = self._get_trial_protocol_id(request)
- trial_protocol_name = self._get_trial_protocol_name(request)
+ async with self.track_session(request.user) as session:
+ source_server = await self._get_dicom_server(request, ae_title)
+ pseudonym = self._get_pseudonym(request)
+ trial_protocol_id = self._get_trial_protocol_id(request)
+ trial_protocol_name = self._get_trial_protocol_name(request)
+
+ query = self.query.copy()
+ query["StudyInstanceUID"] = study_uid
- query = self.query.copy()
- query["StudyInstanceUID"] = study_uid
+ images = wado_retrieve(
+ source_server,
+ query,
+ "STUDY",
+ pseudonym=pseudonym,
+ trial_protocol_id=trial_protocol_id,
+ trial_protocol_name=trial_protocol_name,
+ )
+ metadata = await self.extract_metadata(images)
- images = wado_retrieve(
- source_server,
- query,
- "STUDY",
- pseudonym=pseudonym,
- trial_protocol_id=trial_protocol_id,
- trial_protocol_name=trial_protocol_name,
- )
- metadata = await self.extract_metadata(images)
+ response_data = {"metadata": metadata}
+ await self._finalize_statistic(
+ session, transfer_size=len(json.dumps(response_data).encode())
+ )
- renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
- return Response({"metadata": metadata}, content_type=renderer.content_type)
+ renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
+ return Response(response_data, content_type=renderer.content_type)
class RetrieveSeriesAPIView(RetrieveAPIView):
async def get(
self, request: AuthenticatedApiRequest, ae_title: str, study_uid: str, series_uid: str
) -> Response | StreamingHttpResponse:
- source_server = await self._get_dicom_server(request, ae_title)
- pseudonym = self._get_pseudonym(request)
- trial_protocol_id = self._get_trial_protocol_id(request)
- trial_protocol_name = self._get_trial_protocol_name(request)
-
- query = self.query.copy()
- query["StudyInstanceUID"] = study_uid
- query["SeriesInstanceUID"] = series_uid
-
- images = wado_retrieve(
- source_server,
- query,
- "SERIES",
- pseudonym=pseudonym,
- trial_protocol_id=trial_protocol_id,
- trial_protocol_name=trial_protocol_name,
- )
- images = await self.peek_images(images)
-
- renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
- return StreamingHttpResponse(
- streaming_content=renderer.render(images),
- content_type=renderer.content_type,
- )
+ async with self.track_session(request.user) as session:
+ source_server = await self._get_dicom_server(request, ae_title)
+ pseudonym = self._get_pseudonym(request)
+ trial_protocol_id = self._get_trial_protocol_id(request)
+ trial_protocol_name = self._get_trial_protocol_name(request)
+
+ query = self.query.copy()
+ query["StudyInstanceUID"] = study_uid
+ query["SeriesInstanceUID"] = series_uid
+
+ images = wado_retrieve(
+ source_server,
+ query,
+ "SERIES",
+ pseudonym=pseudonym,
+ trial_protocol_id=trial_protocol_id,
+ trial_protocol_name=trial_protocol_name,
+ )
+ images = await self.peek_images(images)
+
+ renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
+ return StreamingHttpResponse(
+ streaming_content=_StreamingSessionWrapper(
+ renderer.render(images), session, self._finalize_statistic
+ ),
+ content_type=renderer.content_type,
+ )
class RetrieveSeriesMetadataAPIView(RetrieveSeriesAPIView):
async def get(
self, request: AuthenticatedApiRequest, ae_title: str, study_uid: str, series_uid: str
) -> Response:
- source_server = await self._get_dicom_server(request, ae_title)
- pseudonym = self._get_pseudonym(request)
- trial_protocol_id = self._get_trial_protocol_id(request)
- trial_protocol_name = self._get_trial_protocol_name(request)
+ async with self.track_session(request.user) as session:
+ source_server = await self._get_dicom_server(request, ae_title)
+ pseudonym = self._get_pseudonym(request)
+ trial_protocol_id = self._get_trial_protocol_id(request)
+ trial_protocol_name = self._get_trial_protocol_name(request)
- query = self.query.copy()
- query["StudyInstanceUID"] = study_uid
- query["SeriesInstanceUID"] = series_uid
+ query = self.query.copy()
+ query["StudyInstanceUID"] = study_uid
+ query["SeriesInstanceUID"] = series_uid
+
+ images = wado_retrieve(
+ source_server,
+ query,
+ "SERIES",
+ pseudonym=pseudonym,
+ trial_protocol_id=trial_protocol_id,
+ trial_protocol_name=trial_protocol_name,
+ )
+ metadata = await self.extract_metadata(images)
- images = wado_retrieve(
- source_server,
- query,
- "SERIES",
- pseudonym=pseudonym,
- trial_protocol_id=trial_protocol_id,
- trial_protocol_name=trial_protocol_name,
- )
- metadata = await self.extract_metadata(images)
+ response_data = {"metadata": metadata}
+ await self._finalize_statistic(
+ session, transfer_size=len(json.dumps(response_data).encode())
+ )
- renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
- return Response({"metadata": metadata}, content_type=renderer.content_type)
+ renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
+ return Response(response_data, content_type=renderer.content_type)
class RetrieveImageAPIView(RetrieveAPIView):
@@ -346,31 +430,34 @@ async def get(
series_uid: str,
image_uid: str,
) -> Response | StreamingHttpResponse:
- source_server = await self._get_dicom_server(request, ae_title)
- pseudonym = self._get_pseudonym(request)
- trial_protocol_id = self._get_trial_protocol_id(request)
- trial_protocol_name = self._get_trial_protocol_name(request)
-
- query = self.query.copy()
- query["StudyInstanceUID"] = study_uid
- query["SeriesInstanceUID"] = series_uid
- query["SOPInstanceUID"] = image_uid
-
- images = wado_retrieve(
- source_server,
- query,
- "IMAGE",
- pseudonym=pseudonym,
- trial_protocol_id=trial_protocol_id,
- trial_protocol_name=trial_protocol_name,
- )
- images = await self.peek_images(images)
-
- renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
- return StreamingHttpResponse(
- streaming_content=renderer.render(images),
- content_type=renderer.content_type,
- )
+ async with self.track_session(request.user) as session:
+ source_server = await self._get_dicom_server(request, ae_title)
+ pseudonym = self._get_pseudonym(request)
+ trial_protocol_id = self._get_trial_protocol_id(request)
+ trial_protocol_name = self._get_trial_protocol_name(request)
+
+ query = self.query.copy()
+ query["StudyInstanceUID"] = study_uid
+ query["SeriesInstanceUID"] = series_uid
+ query["SOPInstanceUID"] = image_uid
+
+ images = wado_retrieve(
+ source_server,
+ query,
+ "IMAGE",
+ pseudonym=pseudonym,
+ trial_protocol_id=trial_protocol_id,
+ trial_protocol_name=trial_protocol_name,
+ )
+ images = await self.peek_images(images)
+
+ renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
+ return StreamingHttpResponse(
+ streaming_content=_StreamingSessionWrapper(
+ renderer.render(images), session, self._finalize_statistic
+ ),
+ content_type=renderer.content_type,
+ )
class RetrieveImageMetadataAPIView(RetrieveImageAPIView):
@@ -382,28 +469,34 @@ async def get(
series_uid: str,
image_uid: str,
) -> Response:
- source_server = await self._get_dicom_server(request, ae_title)
- pseudonym = self._get_pseudonym(request)
- trial_protocol_id = self._get_trial_protocol_id(request)
- trial_protocol_name = self._get_trial_protocol_name(request)
-
- query = self.query.copy()
- query["StudyInstanceUID"] = study_uid
- query["SeriesInstanceUID"] = series_uid
- query["SOPInstanceUID"] = image_uid
-
- images = wado_retrieve(
- source_server,
- query,
- "IMAGE",
- pseudonym=pseudonym,
- trial_protocol_id=trial_protocol_id,
- trial_protocol_name=trial_protocol_name,
- )
- metadata = await self.extract_metadata(images)
-
- renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
- return Response({"metadata": metadata}, content_type=renderer.content_type)
+ async with self.track_session(request.user) as session:
+ source_server = await self._get_dicom_server(request, ae_title)
+ pseudonym = self._get_pseudonym(request)
+ trial_protocol_id = self._get_trial_protocol_id(request)
+ trial_protocol_name = self._get_trial_protocol_name(request)
+
+ query = self.query.copy()
+ query["StudyInstanceUID"] = study_uid
+ query["SeriesInstanceUID"] = series_uid
+ query["SOPInstanceUID"] = image_uid
+
+ images = wado_retrieve(
+ source_server,
+ query,
+ "IMAGE",
+ pseudonym=pseudonym,
+ trial_protocol_id=trial_protocol_id,
+ trial_protocol_name=trial_protocol_name,
+ )
+ metadata = await self.extract_metadata(images)
+
+ response_data = {"metadata": metadata}
+ await self._finalize_statistic(
+ session, transfer_size=len(json.dumps(response_data).encode())
+ )
+
+ renderer = cast(DicomWebWadoRenderer, getattr(request, "accepted_renderer"))
+ return Response(response_data, content_type=renderer.content_type)
# TODO: respect permission can_store
@@ -416,43 +509,47 @@ async def post(
ae_title: str,
study_uid: str = "",
) -> Response:
- content_type: str = request.content_type
- if not media_type_matches("multipart/related; type=application/dicom", content_type):
- raise UnsupportedMediaType(content_type)
-
- dest_server = await self._get_dicom_server(request, ae_title, "destination")
+ async with self.track_session(request.user) as session:
+ content_type: str = request.content_type
+ if not media_type_matches("multipart/related; type=application/dicom", content_type):
+ raise UnsupportedMediaType(content_type)
- results: Dataset = Dataset()
- results.ReferencedSOPSequence = Sequence([])
- results.FailedSOPSequence = Sequence([])
-
- async for ds in parse_request_in_chunks(request):
- if ds is None:
- continue
+ dest_server = await self._get_dicom_server(request, ae_title, "destination")
- logger.debug(f"Received instance {ds.SOPInstanceUID} via STOW-RS")
+ results: Dataset = Dataset()
+ results.ReferencedSOPSequence = Sequence([])
+ results.FailedSOPSequence = Sequence([])
- if study_uid:
- results.RetrieveURL = reverse(
- "wado_rs-study_with_study_uid",
- args=[dest_server.ae_title, ds.StudyInstanceUID],
- )
- if ds.StudyInstanceUID != study_uid:
+ async for ds in parse_request_in_chunks(request):
+ if ds is None:
continue
- logger.debug(f"Storing instance {ds.SOPInstanceUID} to {dest_server.ae_title}")
- result_ds, failed = await stow_store(dest_server, ds)
-
- if failed:
- logger.debug(
- f"Storing instance {ds.SOPInstanceUID} to {dest_server.ae_title} failed"
- )
- results.FailedSOPSequence.append(result_ds)
- else:
- results.ReferencedSOPSequence.append(result_ds)
- logger.debug(
- f"Successfully stored instance {ds.SOPInstanceUID} to {dest_server.ae_title}"
- )
+ logger.debug(f"Received instance {ds.SOPInstanceUID} via STOW-RS")
+
+ if study_uid:
+ results.RetrieveURL = reverse(
+ "wado_rs-study_with_study_uid",
+ args=[dest_server.ae_title, ds.StudyInstanceUID],
+ )
+ if ds.StudyInstanceUID != study_uid:
+ continue
+
+ logger.debug(f"Storing instance {ds.SOPInstanceUID} to {dest_server.ae_title}")
+ result_ds, failed = await stow_store(dest_server, ds)
+
+ if failed:
+ logger.debug(
+ f"Storing instance {ds.SOPInstanceUID} to {dest_server.ae_title} failed"
+ )
+ results.FailedSOPSequence.append(result_ds)
+ else:
+ results.ReferencedSOPSequence.append(result_ds)
+ logger.debug(f"Stored instance {ds.SOPInstanceUID} to {dest_server.ae_title}")
+
+ response_data = [results]
+ await self._finalize_statistic(
+ session, transfer_size=len(json.dumps(response_data[0].to_json_dict()).encode())
+ )
- assert request.accepted_renderer is not None
- return Response([results], content_type=request.accepted_renderer.media_type)
+ assert request.accepted_renderer is not None
+ return Response(response_data, content_type=request.accepted_renderer.media_type)