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.
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/PKG-INFO +1 -1
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/api/schemas.md +19 -6
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/quickstart.md +1 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/image_notification.py +11 -7
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/client.py +18 -2
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/mock_server.py +7 -3
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.github/workflows/docs.yml +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.github/workflows/publish.yaml +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.gitignore +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/LICENSE +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/README.md +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/api/client.md +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/api/mock-server.md +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/docs/index.md +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/fetch_and_cast_results.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/local_dev.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/patch_result.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/primitive_result.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/mkdocs.yml +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/pyproject.toml +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/__init__.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/__init__.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/common.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/image.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/info.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/invocation.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/quality_control.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/registration.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/schemas/results.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/__init__.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/decode_images.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/errors.py +0 -0
- {sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/utils/helpers.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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)
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/image_notification.py
RENAMED
|
@@ -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
|
-
"""
|
|
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.
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/client.py
RENAMED
|
@@ -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
|
)
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/mock_server.py
RENAMED
|
@@ -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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.github/workflows/docs.yml
RENAMED
|
File without changes
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/.github/workflows/publish.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/patch_result.py
RENAMED
|
File without changes
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/examples/primitive_result.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sectra_image_analysis_api-1.6.0 → sectra_image_analysis_api-1.6.2}/src/sectra_client/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|