sectra-image-analysis-api 1.6.0__tar.gz → 1.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/PKG-INFO +1 -1
  2. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/api/schemas.md +19 -6
  3. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/quickstart.md +1 -0
  4. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/image_notification.py +11 -7
  5. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/client.py +18 -2
  6. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/mock_server.py +7 -3
  7. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.github/workflows/docs.yml +0 -0
  8. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.github/workflows/publish.yaml +0 -0
  9. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.gitignore +0 -0
  10. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/LICENSE +0 -0
  11. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/README.md +0 -0
  12. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/api/client.md +0 -0
  13. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/api/mock-server.md +0 -0
  14. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/index.md +0 -0
  15. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/fetch_and_cast_results.py +0 -0
  16. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/local_dev.py +0 -0
  17. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/patch_result.py +0 -0
  18. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/primitive_result.py +0 -0
  19. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/mkdocs.yml +0 -0
  20. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/pyproject.toml +0 -0
  21. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/__init__.py +0 -0
  22. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/__init__.py +0 -0
  23. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/common.py +0 -0
  24. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/image.py +0 -0
  25. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/info.py +0 -0
  26. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/invocation.py +0 -0
  27. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/quality_control.py +0 -0
  28. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/registration.py +0 -0
  29. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/results.py +0 -0
  30. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/__init__.py +0 -0
  31. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/decode_images.py +0 -0
  32. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/errors.py +0 -0
  33. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/helpers.py +0 -0
  34. {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sectra-image-analysis-api
3
- Version: 1.6.0
3
+ Version: 1.6.2
4
4
  Summary: A wrapper for the Sectra Image Analysis API.
5
5
  Project-URL: Homepage, https://github.com/amspath/sectra-image-analysis-api
6
6
  Project-URL: Repository, https://github.com/amspath/sectra-image-analysis-api
@@ -76,21 +76,34 @@ Another discriminated union on `type`:
76
76
 
77
77
  ### Image notifications
78
78
 
79
- `NewImageFilesInvocation` (`action: "newImageFiles"`) is sent when new image files are imported for a slide, so your app can decide whether the slide is worth analysing. Unlike the other four actions it carries the slide's full `imageInfo` (PHI-free) up front, so the decision needs no API calls.
79
+ `NewImageFilesInvocation` (`action: "newImageFiles"`) is sent when new image files are imported for a slide, so your app can decide whether the slide is worth analysing. Unlike the other four actions it carries the slide's full `imageInfo` (PHI-free) up front.
80
80
 
81
- Sectra delivers it to whatever URL you registered, so it normally arrives on the same webhook as everything else.
81
+ It does not arrive on the URL you registered, as Sectra appends `/imagenotification` to it:
82
+
83
+ | Action | Delivered to |
84
+ |---|---|
85
+ | `create` / `modify` / `cancel` / `delete` | the registered URL, e.g. `/sectra/hook` |
86
+ | `newImageFiles` | `<registered URL>/imagenotification` |
87
+
88
+ So an app needs a second route.
82
89
 
83
90
  ```python
84
91
  @app.post("/sectra/hook")
85
92
  def hook(invocation: Invocation):
86
- if isinstance(invocation, NewImageFilesInvocation):
87
- if invocation.imageInfo.micronsPerPixel > 0.5:
88
- return {} # too low-res for us — ignore
89
- ... # otherwise queue the analysis
93
+ ... # create / modify / cancel / delete
94
+
95
+
96
+ @app.post("/sectra/hook/imagenotification")
97
+ def image_notification(invocation: NewImageFilesInvocation):
98
+ if invocation.imageInfo.micronsPerPixel > 0.5:
99
+ return {} # too low-res for us — ignore
100
+ ... # otherwise queue the analysis
90
101
  ```
91
102
 
92
103
  To simulate one in local development, use [`MockSectraServer.notify()`](mock-server.md): it builds the payload from the mock's own slide metadata, so the notification always agrees with what the mock later serves.
93
104
 
105
+ Pass the registered URL — `notify()` appends `/imagenotification` itself, just like Sectra.
106
+
94
107
  ```python
95
108
  mock.notify(webhook_url="http://localhost:8000/sectra/hook", slide_id="slide-001")
96
109
  ```
@@ -71,6 +71,7 @@ from sectra_client import SectraClient
71
71
  from sectra_client.schemas import Result, ResultData, Invocation, PrimitiveResultContent, CreateInvocation
72
72
 
73
73
  app = FastAPI()
74
+
74
75
  @app.post("/sectra/hook")
75
76
  def hook(invocation: Invocation, background_tasks: BackgroundTasks):
76
77
  background_tasks.add_task(run_analysis, invocation)
@@ -4,6 +4,10 @@ Sectra sends a ``newImageFiles`` notification when new image files are imported.
4
4
  carries the slide's full (PHI-free) ``imageInfo``, so the triage decision needs no API
5
5
  calls; and ignoring the notification is a valid response.
6
6
 
7
+ Image notifications do NOT arrive on the URL you registered. Sectra appends
8
+ ``/imagenotification`` to it, so an app registered at ``/sectra/hook`` needs a second
9
+ route at ``/sectra/hook/imagenotification``.
10
+
7
11
  Run it directly; it starts a mock Sectra server and an analysis app and fires two
8
12
  notifications, one that gets accepted and one that gets skipped.
9
13
 
@@ -33,15 +37,14 @@ def _is_interesting(info: ImageMetadata) -> bool:
33
37
 
34
38
  @analysis_app.post("/sectra/hook")
35
39
  def sectra_hook(invocation: Invocation):
36
- """Receive an invocation from Sectra.
40
+ """The registered URL: create / modify / cancel / delete land here."""
41
+ # See local_dev.py for a full create -> download -> store round-trip.
42
+ return {}
37
43
 
38
- Notifications share this endpoint with create/modify/cancel/delete, so handle them
39
- explicitly; an unhandled branch here means notifications are silently dropped.
40
- """
41
- if not isinstance(invocation, NewImageFilesInvocation):
42
- # See local_dev.py for a full create -> download -> store round-trip.
43
- return {}
44
44
 
45
+ @analysis_app.post("/sectra/hook/imagenotification")
46
+ def sectra_image_notification(invocation: NewImageFilesInvocation):
47
+ """The registered URL + /imagenotification: newImageFiles lands here."""
45
48
  info = invocation.imageInfo
46
49
  print(
47
50
  f"[analysis app] {invocation.slideId}: "
@@ -90,6 +93,7 @@ if __name__ == "__main__":
90
93
  app_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
91
94
  app_sock.bind(("localhost", 0))
92
95
  APP_PORT = app_sock.getsockname()[1]
96
+ # The registered URL. notify() appends /imagenotification, as Sectra does.
93
97
  WEBHOOK = f"http://localhost:{APP_PORT}/sectra/hook"
94
98
 
95
99
  # Two slides: one we want, one we don't.
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import pathlib
3
3
  import re
4
+ import time
4
5
  from typing import List, Optional, cast
5
6
  from urllib.parse import urlsplit, urlunsplit
6
7
 
@@ -22,6 +23,11 @@ from sectra_client.utils.helpers import JSONPayload, connection_retry
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
 
26
+ # Server versions change only when Sectra is upgraded, so one lookup per server per hour
27
+ # is plenty. Without this every client construction costs a /info round trip.
28
+ _INFO_TTL_SECONDS = 3600
29
+ _info_cache: dict[str, tuple[float, ApplicationInfo]] = {}
30
+
25
31
  class SectraClient:
26
32
  __slots__ = ("_url", "_token", "version_info", "_headers", "_session")
27
33
 
@@ -96,9 +102,19 @@ class SectraClient:
96
102
  return None
97
103
 
98
104
  def _retrieve_version_info(self) -> ApplicationInfo:
99
- """Retrieves the versions of DPAT from the server."""
105
+ """Retrieves the versions of DPAT from the server.
106
+
107
+ Cached per base url for `_INFO_TTL_SECONDS`, so constructing many clients against
108
+ the same server costs one /info call per hour rather than one per client.
109
+ """
110
+
111
+ cached = _info_cache.get(self._url)
112
+ if cached is not None and time.monotonic() - cached[0] < _INFO_TTL_SECONDS:
113
+ versions = cached[1]
114
+ else:
115
+ versions = ApplicationInfo(**cast(dict, self._get("/info")))
116
+ _info_cache[self._url] = (time.monotonic(), versions)
100
117
 
101
- versions = ApplicationInfo(**cast(dict, self._get("/info")))
102
118
  self._headers.update(
103
119
  {"X-Sectra-ApiVersion": versions.apiVersion, "X-Sectra-SoftwareVersion": versions.softwareVersion}
104
120
  )
@@ -234,6 +234,9 @@ class MockSectraServer:
234
234
  ) -> dict:
235
235
  """Fire a ``newImageFiles`` image notification at *webhook_url*.
236
236
 
237
+ Sectra does not deliver image notifications to the registered URL itself; it
238
+ appends ``/imagenotification`` to it. So does this method.
239
+
237
240
  ``imageInfo`` is taken from this server's own slide metadata (registered via
238
241
  :meth:`add_slide`, or fabricated on demand), so the notification always agrees
239
242
  with what ``GET /slides/{slide_id}/info`` will subsequently return.
@@ -241,8 +244,8 @@ class MockSectraServer:
241
244
  Parameters
242
245
  ----------
243
246
  webhook_url:
244
- Full URL of the analysis app's endpoint, e.g.
245
- ``"http://localhost:8000/sectra/hook"``.
247
+ The app's *registered* URL, e.g. ``"http://localhost:8000/sectra/hook"``.
248
+ The notification is POSTed to ``<webhook_url>/imagenotification``.
246
249
  slide_id:
247
250
  Slide that new image files were imported for.
248
251
  application_id:
@@ -260,7 +263,8 @@ class MockSectraServer:
260
263
  cancellationToken=str(uuid.uuid4()),
261
264
  imageInfo=self._get_or_create_slide(slide_id),
262
265
  )
263
- resp = _requests.post(webhook_url, json=invocation.model_dump(), timeout=30)
266
+ url = f"{webhook_url.rstrip('/')}/imagenotification"
267
+ resp = _requests.post(url, json=invocation.model_dump(), timeout=30)
264
268
  resp.raise_for_status()
265
269
  return resp.json()
266
270