copick-web 0.1.5__py3-none-any.whl
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.
- copick_web/__init__.py +3 -0
- copick_web/app/__init__.py +1 -0
- copick_web/app/config.py +52 -0
- copick_web/app/dependencies.py +38 -0
- copick_web/app/main.py +180 -0
- copick_web/app/models.py +370 -0
- copick_web/app/routes/__init__.py +1 -0
- copick_web/app/routes/config.py +162 -0
- copick_web/app/routes/filaments.py +169 -0
- copick_web/app/routes/projects.py +15 -0
- copick_web/app/routes/runs.py +392 -0
- copick_web/app/routes/zarr_proxy.py +190 -0
- copick_web/app/services/__init__.py +1 -0
- copick_web/app/services/cache.py +103 -0
- copick_web/app/services/compat.py +147 -0
- copick_web/app/services/config_file.py +133 -0
- copick_web/app/services/copick_service.py +757 -0
- copick_web/app/services/filament_picks.py +113 -0
- copick_web/app/services/object_types.py +245 -0
- copick_web/app/services/project_registry.py +465 -0
- copick_web/app/services/registry_client.py +93 -0
- copick_web/app/services/run_info.py +148 -0
- copick_web/app/services/thumbnails.py +132 -0
- copick_web/app/services/zarr_urls.py +47 -0
- copick_web/app/validation.py +46 -0
- copick_web/cli.py +100 -0
- copick_web/static/assets/blosc-D1xNXZJs-CRrdIaHK.js +17 -0
- copick_web/static/assets/chunk-INHXZS53-DiyuLb3Z-DAOLzMPX.js +1 -0
- copick_web/static/assets/index-CFHbVtPG.css +2 -0
- copick_web/static/assets/index-DGooRI34.js +4333 -0
- copick_web/static/assets/lz4-1Ws5oVWR-TBpHcg4u.js +15 -0
- copick_web/static/assets/zstd-C4EcZnjq-_uEBMK3Z.js +15 -0
- copick_web/static/copick-logo.png +0 -0
- copick_web/static/index.html +33 -0
- copick_web-0.1.5.dist-info/METADATA +19 -0
- copick_web-0.1.5.dist-info/RECORD +38 -0
- copick_web-0.1.5.dist-info/WHEEL +4 -0
- copick_web-0.1.5.dist-info/entry_points.txt +2 -0
copick_web/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# copick-web server app
|
copick_web/app/config.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Server configuration using pydantic-settings."""
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Settings(BaseSettings):
|
|
7
|
+
"""Application settings loaded from environment variables."""
|
|
8
|
+
|
|
9
|
+
copick_config_paths: list[str] = []
|
|
10
|
+
registry_url: str | None = None
|
|
11
|
+
registry_refresh_seconds: int = 60
|
|
12
|
+
service_cache_size: int = 8
|
|
13
|
+
# Hard ceiling on a single zarr-chunk read. If exceeded, the request
|
|
14
|
+
# returns 504 and the underlying CopickService is evicted so the next
|
|
15
|
+
# request rebuilds with a fresh SSH connection.
|
|
16
|
+
zarr_read_timeout_seconds: float = 60.0
|
|
17
|
+
# Max concurrent zarr chunk reads per project. Bounds peak memory
|
|
18
|
+
# (≈ value × max_chunk_size) and avoids saturating a single asyncssh
|
|
19
|
+
# SFTP channel — past ~16 in flight, parallel reads stop helping.
|
|
20
|
+
zarr_concurrency_per_project: int = 8
|
|
21
|
+
slurm_user: str | None = None
|
|
22
|
+
slurm_keyfile: str | None = None
|
|
23
|
+
slurm_ssh_port: int = 22
|
|
24
|
+
# cluster_id -> SSH hostname/IP. Defaults mirror the umbrella seed
|
|
25
|
+
# (umbrella/stores/migrations/0010_seed_clusters.py). Override via env:
|
|
26
|
+
# CLUSTER_HOSTS='{"bruno":"…","czii":"…"}'
|
|
27
|
+
cluster_hosts: dict[str, str] = {
|
|
28
|
+
"bruno": "192.168.98.229",
|
|
29
|
+
"czii": "10.50.120.90",
|
|
30
|
+
}
|
|
31
|
+
cors_origins: list[str] = [
|
|
32
|
+
"http://localhost:5173",
|
|
33
|
+
"http://127.0.0.1:5173",
|
|
34
|
+
"http://localhost:8000",
|
|
35
|
+
"http://127.0.0.1:8000",
|
|
36
|
+
]
|
|
37
|
+
host: str = "0.0.0.0"
|
|
38
|
+
port: int = 8000
|
|
39
|
+
base_path: str = ""
|
|
40
|
+
# In-memory caches of derived data (env: THUMBNAIL_CACHE_MB, MEASUREMENT_CACHE_MB, CACHE_MAX_AGE_SECONDS,
|
|
41
|
+
# CACHE_SWEEP_SECONDS). Entries older than the max age are dropped by a sweep every CACHE_SWEEP_SECONDS.
|
|
42
|
+
thumbnail_cache_mb: int = 64
|
|
43
|
+
measurement_cache_mb: int = 512
|
|
44
|
+
cache_max_age_seconds: int = 3600
|
|
45
|
+
cache_sweep_seconds: int = 300
|
|
46
|
+
|
|
47
|
+
class Config:
|
|
48
|
+
env_file = ".env"
|
|
49
|
+
env_file_encoding = "utf-8"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
settings = Settings()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""FastAPI dependency injection for project-scoped routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException, Path
|
|
4
|
+
|
|
5
|
+
from .models import ProjectSummaryResponse
|
|
6
|
+
from .services.copick_service import CopickService
|
|
7
|
+
from .services.project_registry import ProjectRegistry, get_project_registry
|
|
8
|
+
from .services.registry_client import RegistryError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_registry() -> ProjectRegistry:
|
|
12
|
+
"""Dependency: the application's ``ProjectRegistry``."""
|
|
13
|
+
return get_project_registry()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_copick_service(project_id: str = Path(...)) -> CopickService:
|
|
17
|
+
"""Dependency: resolve the ``CopickService`` for the path's ``project_id``."""
|
|
18
|
+
registry = get_project_registry()
|
|
19
|
+
try:
|
|
20
|
+
return registry.get_service(project_id)
|
|
21
|
+
except KeyError:
|
|
22
|
+
raise HTTPException(status_code=404, detail=f"Project '{project_id}' not found") from None
|
|
23
|
+
except RegistryError as e:
|
|
24
|
+
raise HTTPException(status_code=502, detail=str(e)) from e
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_project_meta(project_id: str = Path(...)) -> ProjectSummaryResponse:
|
|
28
|
+
"""Dependency: resolve the ``ProjectSummaryResponse`` for the path's ``project_id``.
|
|
29
|
+
|
|
30
|
+
Used by routes that need to branch on ``source`` (local vs. registry) — e.g.
|
|
31
|
+
zarr URL construction, where registry projects skip the server-side proxy
|
|
32
|
+
and point the browser at ``data_url`` directly.
|
|
33
|
+
"""
|
|
34
|
+
registry = get_project_registry()
|
|
35
|
+
try:
|
|
36
|
+
return registry.get_metadata(project_id)
|
|
37
|
+
except KeyError:
|
|
38
|
+
raise HTTPException(status_code=404, detail=f"Project '{project_id}' not found") from None
|
copick_web/app/main.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""FastAPI application entry point."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
from fastapi.responses import FileResponse, RedirectResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
|
|
13
|
+
from .config import settings
|
|
14
|
+
from .routes import config, filaments, projects, runs, zarr_proxy
|
|
15
|
+
from .services.project_registry import ProjectRegistry, set_project_registry
|
|
16
|
+
from .services.registry_client import RegistryClient
|
|
17
|
+
|
|
18
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _refresh_loop(registry: ProjectRegistry, interval: int):
|
|
23
|
+
"""Background loop that refreshes the registry list at a fixed cadence."""
|
|
24
|
+
while True:
|
|
25
|
+
try:
|
|
26
|
+
await asyncio.sleep(interval)
|
|
27
|
+
await registry.refresh_registry()
|
|
28
|
+
except asyncio.CancelledError:
|
|
29
|
+
raise
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logger.exception("Registry refresh loop error: %s", e)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@asynccontextmanager
|
|
35
|
+
async def lifespan(app: FastAPI):
|
|
36
|
+
"""Application lifespan handler."""
|
|
37
|
+
# Build registry
|
|
38
|
+
if not settings.copick_config_paths and not settings.registry_url:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
"No project source configured. Provide local config paths via the CLI / "
|
|
41
|
+
"COPICK_CONFIG_PATHS, or set REGISTRY_URL."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
client = RegistryClient(settings.registry_url) if settings.registry_url else None
|
|
45
|
+
registry = ProjectRegistry(registry_client=client, service_cache_size=settings.service_cache_size)
|
|
46
|
+
|
|
47
|
+
for path in settings.copick_config_paths:
|
|
48
|
+
try:
|
|
49
|
+
pid = registry.register_local(path)
|
|
50
|
+
logger.info(
|
|
51
|
+
"Registered local project '%s' (%s) from %s",
|
|
52
|
+
pid,
|
|
53
|
+
registry.get_metadata(pid).name or "unnamed",
|
|
54
|
+
path,
|
|
55
|
+
)
|
|
56
|
+
except FileNotFoundError as e:
|
|
57
|
+
logger.error("Local config not found, skipping: %s (%s)", path, e)
|
|
58
|
+
|
|
59
|
+
if client is not None:
|
|
60
|
+
try:
|
|
61
|
+
await registry.refresh_registry()
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.warning("Initial registry refresh failed; continuing without registry projects: %s", e)
|
|
64
|
+
|
|
65
|
+
set_project_registry(registry)
|
|
66
|
+
|
|
67
|
+
tasks = [asyncio.create_task(_sweep_caches(registry, settings.cache_sweep_seconds))]
|
|
68
|
+
if client is not None:
|
|
69
|
+
tasks.append(asyncio.create_task(_refresh_loop(registry, settings.registry_refresh_seconds)))
|
|
70
|
+
|
|
71
|
+
logger.info("copick-web ready: %d project(s) listed.", len(registry.list_projects()))
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
yield
|
|
75
|
+
finally:
|
|
76
|
+
for task in tasks:
|
|
77
|
+
task.cancel()
|
|
78
|
+
try:
|
|
79
|
+
await task
|
|
80
|
+
except asyncio.CancelledError:
|
|
81
|
+
pass
|
|
82
|
+
logger.info("Shutting down copick-web server")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def _sweep_caches(registry: ProjectRegistry, every: float) -> None:
|
|
86
|
+
"""Drop expired cache entries of every open project regularly, so memory is released even when nothing is
|
|
87
|
+
requested."""
|
|
88
|
+
while True:
|
|
89
|
+
await asyncio.sleep(max(every, 1))
|
|
90
|
+
try:
|
|
91
|
+
dropped = registry.sweep_caches()
|
|
92
|
+
if dropped:
|
|
93
|
+
logger.info(f"Dropped {dropped} expired cache entries")
|
|
94
|
+
except Exception as e: # never let the sweeper die
|
|
95
|
+
logger.warning(f"Cache sweep failed: {e}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
app = FastAPI(
|
|
99
|
+
title="Copick Web Server",
|
|
100
|
+
description="API server for copick web visualization",
|
|
101
|
+
version="0.1.0",
|
|
102
|
+
lifespan=lifespan,
|
|
103
|
+
# A prefix the proxy forwards (e.g. /node/<host>/<port>); requests with or without it are routed alike.
|
|
104
|
+
root_path=settings.base_path.rstrip("/"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.middleware("http")
|
|
109
|
+
async def redirect_root_relatively(request: Request, call_next):
|
|
110
|
+
"""Add the trailing slash to the app root under a forwarded prefix (``/node/<host>/<port>``) with a relative
|
|
111
|
+
redirect: Starlette's own is absolute and names the Host it was sent, which may be the backend behind the proxy."""
|
|
112
|
+
root = request.scope.get("root_path", "")
|
|
113
|
+
if root and request.url.path == root:
|
|
114
|
+
query = f"?{request.url.query}" if request.url.query else ""
|
|
115
|
+
return RedirectResponse(f"{root.rsplit('/', 1)[-1]}/{query}", status_code=307)
|
|
116
|
+
return await call_next(request)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Configure CORS
|
|
120
|
+
app.add_middleware(
|
|
121
|
+
CORSMiddleware,
|
|
122
|
+
allow_origins=settings.cors_origins,
|
|
123
|
+
allow_credentials=True,
|
|
124
|
+
allow_methods=["*"],
|
|
125
|
+
allow_headers=["*"],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
app.include_router(projects.router)
|
|
129
|
+
app.include_router(config.router)
|
|
130
|
+
app.include_router(runs.router)
|
|
131
|
+
app.include_router(filaments.router)
|
|
132
|
+
app.include_router(zarr_proxy.router)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.get("/health")
|
|
136
|
+
async def health_check():
|
|
137
|
+
"""Liveness check. Async so a saturated threadpool can't make the server look dead."""
|
|
138
|
+
return {"status": "healthy"}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Serve the built SPA. Order matters: SPA catch-all must come BEFORE the
|
|
142
|
+
# StaticFiles mount at "/" so it can intercept deep-link refreshes
|
|
143
|
+
# (e.g. /projects/abc) and return index.html instead of 404.
|
|
144
|
+
static_dir = Path(__file__).parent.parent / "static"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _has_static_files() -> bool:
|
|
148
|
+
if not static_dir.exists():
|
|
149
|
+
return False
|
|
150
|
+
return any(f.name != ".gitkeep" for f in static_dir.iterdir())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if _has_static_files():
|
|
154
|
+
index_file = static_dir / "index.html"
|
|
155
|
+
|
|
156
|
+
@app.get("/projects/{full_path:path}", include_in_schema=False)
|
|
157
|
+
def spa_projects(full_path: str, request: Request):
|
|
158
|
+
"""Serve the SPA index.html for client-side routes under /projects."""
|
|
159
|
+
if not index_file.exists():
|
|
160
|
+
raise HTTPException(status_code=404, detail="SPA index.html missing")
|
|
161
|
+
return FileResponse(index_file)
|
|
162
|
+
|
|
163
|
+
logger.info(f"Serving static files from: {static_dir}")
|
|
164
|
+
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
|
|
165
|
+
elif static_dir.exists():
|
|
166
|
+
logger.warning(
|
|
167
|
+
f"Static directory exists but is empty: {static_dir}. "
|
|
168
|
+
"Run 'npm run build:server' in the client directory to build the client."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
import uvicorn
|
|
174
|
+
|
|
175
|
+
uvicorn.run(
|
|
176
|
+
"copick_web.app.main:app",
|
|
177
|
+
host=settings.host,
|
|
178
|
+
port=settings.port,
|
|
179
|
+
reload=True,
|
|
180
|
+
)
|
copick_web/app/models.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Pydantic response models for the API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
SegmentationType = Literal["binary", "multilabel", "instance", "panoptic"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FeaturesResponse(BaseModel):
|
|
11
|
+
"""Optional features of the installed copick (see services/compat.py)."""
|
|
12
|
+
|
|
13
|
+
filaments: bool = False
|
|
14
|
+
segmentation_types: bool = False
|
|
15
|
+
pick_identity: bool = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProjectSummaryResponse(BaseModel):
|
|
19
|
+
"""Summary of a project tracked by the project registry."""
|
|
20
|
+
|
|
21
|
+
id: str
|
|
22
|
+
source: Literal["registry", "local"]
|
|
23
|
+
# Registry-derived fields (all optional — locals don't have them).
|
|
24
|
+
session_name: Optional[str] = None
|
|
25
|
+
run_name: Optional[str] = None
|
|
26
|
+
cluster_id: Optional[str] = None
|
|
27
|
+
scope: Optional[str] = None
|
|
28
|
+
status: Optional[str] = None
|
|
29
|
+
created_at: Optional[str] = None
|
|
30
|
+
proc_run_id: Optional[int] = None
|
|
31
|
+
config_url: Optional[str] = None
|
|
32
|
+
data_url: Optional[str] = None
|
|
33
|
+
root_url: Optional[str] = None
|
|
34
|
+
# Pulled from copick config when service is materialized; nullable so listing stays cheap.
|
|
35
|
+
name: Optional[str] = None
|
|
36
|
+
description: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConfigResponse(BaseModel):
|
|
40
|
+
"""Project configuration response."""
|
|
41
|
+
|
|
42
|
+
name: Optional[str]
|
|
43
|
+
description: Optional[str]
|
|
44
|
+
version: Optional[str]
|
|
45
|
+
user_id: Optional[str]
|
|
46
|
+
session_id: Optional[str]
|
|
47
|
+
features: FeaturesResponse = FeaturesResponse()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PickableObjectResponse(BaseModel):
|
|
51
|
+
"""Pickable object metadata."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
is_particle: bool
|
|
55
|
+
label: Optional[int]
|
|
56
|
+
color: tuple[int, int, int, int]
|
|
57
|
+
radius: Optional[float]
|
|
58
|
+
is_filament: bool = False
|
|
59
|
+
filament: Optional[dict[str, Any]] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FilamentSpecModel(BaseModel):
|
|
63
|
+
"""Filament declaration of an object (copick ``FilamentSpec``)."""
|
|
64
|
+
|
|
65
|
+
polar: Optional[bool] = None
|
|
66
|
+
helical_rise_a: Optional[float] = None
|
|
67
|
+
helical_twist_deg: Optional[float] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ObjectTypeFields(BaseModel):
|
|
71
|
+
"""The editable fields of a pickable object type."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
is_particle: bool = True
|
|
75
|
+
label: Optional[int] = None
|
|
76
|
+
color: tuple[int, int, int, int]
|
|
77
|
+
radius: Optional[float] = None
|
|
78
|
+
map_threshold: Optional[float] = None
|
|
79
|
+
emdb_id: Optional[str] = None
|
|
80
|
+
pdb_id: Optional[str] = None
|
|
81
|
+
identifier: Optional[str] = None
|
|
82
|
+
#: None: not a filament.
|
|
83
|
+
filament: Optional[FilamentSpecModel] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ObjectTypeRequest(ObjectTypeFields):
|
|
87
|
+
"""Create or update an object type; ``version`` is the object list version the change was made against."""
|
|
88
|
+
|
|
89
|
+
version: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ObjectTypesResponse(BaseModel):
|
|
93
|
+
"""The configuration's object types, for editing."""
|
|
94
|
+
|
|
95
|
+
version: str
|
|
96
|
+
editable: bool
|
|
97
|
+
config_file: Optional[str]
|
|
98
|
+
suggested_label: int
|
|
99
|
+
objects: list[ObjectTypeFields]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ReloadResponse(BaseModel):
|
|
103
|
+
"""The project after it was re-opened from its configuration file."""
|
|
104
|
+
|
|
105
|
+
runs: int
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class RunSummaryResponse(BaseModel):
|
|
109
|
+
"""Summary of a run."""
|
|
110
|
+
|
|
111
|
+
name: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TomogramSummaryResponse(BaseModel):
|
|
115
|
+
"""Summary of a tomogram."""
|
|
116
|
+
|
|
117
|
+
tomo_type: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class VoxelSpacingSummaryResponse(BaseModel):
|
|
121
|
+
"""Summary of a voxel spacing with its tomograms."""
|
|
122
|
+
|
|
123
|
+
voxel_size: float
|
|
124
|
+
tomograms: list[TomogramSummaryResponse]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class RunDetailResponse(BaseModel):
|
|
128
|
+
"""Detailed run information including voxel spacings."""
|
|
129
|
+
|
|
130
|
+
name: str
|
|
131
|
+
voxel_spacings: list[VoxelSpacingSummaryResponse]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ZarrInfo(BaseModel):
|
|
135
|
+
shape: list[int]
|
|
136
|
+
dtype: str
|
|
137
|
+
levels: int
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TomogramInfo(BaseModel):
|
|
141
|
+
tomo_type: str
|
|
142
|
+
#: Where the tomogram is read from (portal location, overlay or static copy).
|
|
143
|
+
path: Optional[str] = None
|
|
144
|
+
#: Level-0 shape (z, y, x), dtype and pyramid levels; None if the store could not be read.
|
|
145
|
+
zarr: Optional[ZarrInfo] = None
|
|
146
|
+
#: CryoET Data Portal metadata (id, url, name, reconstruction, deposition, authors, paths...), if any.
|
|
147
|
+
portal: Optional[dict[str, Any]] = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class VoxelSpacingInfo(BaseModel):
|
|
151
|
+
voxel_size: float
|
|
152
|
+
static_path: Optional[str] = None
|
|
153
|
+
overlay_path: Optional[str] = None
|
|
154
|
+
portal_id: Optional[int] = None
|
|
155
|
+
tomograms: list[TomogramInfo]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class RunPortalInfo(BaseModel):
|
|
159
|
+
run_id: int
|
|
160
|
+
run_name: Optional[str] = None
|
|
161
|
+
run_url: str
|
|
162
|
+
dataset_id: Optional[int] = None
|
|
163
|
+
dataset_url: Optional[str] = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RunInfoResponse(BaseModel):
|
|
167
|
+
"""Where a run's data lives and what it holds (the run info dialog)."""
|
|
168
|
+
|
|
169
|
+
name: str
|
|
170
|
+
backend: str
|
|
171
|
+
static_path: Optional[str] = None
|
|
172
|
+
overlay_path: Optional[str] = None
|
|
173
|
+
portal: Optional[RunPortalInfo] = None
|
|
174
|
+
counts: dict[str, Optional[int]]
|
|
175
|
+
voxel_spacings: list[VoxelSpacingInfo]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TomogramResponse(BaseModel):
|
|
179
|
+
"""Tomogram with zarr URL."""
|
|
180
|
+
|
|
181
|
+
tomo_type: str
|
|
182
|
+
zarr_url: str
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class PointResponse(BaseModel):
|
|
186
|
+
"""A single pick point.
|
|
187
|
+
|
|
188
|
+
``x``, ``y``, ``z`` are the stored location in Angstrom. The particle centre is the location plus the translation
|
|
189
|
+
of ``transformation`` (a 4x4 object-to-tomogram matrix); clients draw the centre and send the location and
|
|
190
|
+
transformation back unchanged when saving.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
x: float
|
|
194
|
+
y: float
|
|
195
|
+
z: float
|
|
196
|
+
instance_id: Optional[int]
|
|
197
|
+
score: Optional[float]
|
|
198
|
+
transformation: Optional[list[list[float]]] = None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class PicksSummaryResponse(BaseModel):
|
|
202
|
+
"""Summary of a picks collection."""
|
|
203
|
+
|
|
204
|
+
object_name: str
|
|
205
|
+
user_id: str
|
|
206
|
+
session_id: str
|
|
207
|
+
point_count: int
|
|
208
|
+
color: tuple[int, int, int, int]
|
|
209
|
+
is_filament: bool = False
|
|
210
|
+
instance_count: int = 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class PicksDetailResponse(BaseModel):
|
|
214
|
+
"""Detailed picks with all points."""
|
|
215
|
+
|
|
216
|
+
object_name: str
|
|
217
|
+
user_id: str
|
|
218
|
+
session_id: str
|
|
219
|
+
color: tuple[int, int, int, int]
|
|
220
|
+
points: list[PointResponse]
|
|
221
|
+
is_filament: bool = False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class SegmentationSummaryResponse(BaseModel):
|
|
225
|
+
"""Summary of a segmentation."""
|
|
226
|
+
|
|
227
|
+
name: str
|
|
228
|
+
user_id: str
|
|
229
|
+
session_id: str
|
|
230
|
+
voxel_size: float
|
|
231
|
+
is_multilabel: bool
|
|
232
|
+
zarr_url: str
|
|
233
|
+
color: Optional[tuple[int, int, int, int]]
|
|
234
|
+
segmentation_type: SegmentationType = "binary"
|
|
235
|
+
is_instance: bool = False
|
|
236
|
+
is_panoptic: bool = False
|
|
237
|
+
channels: Optional[list[str]] = None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class InstanceResponse(BaseModel):
|
|
241
|
+
"""One instance (or panoptic segment) of a segmentation, measured on a pyramid level."""
|
|
242
|
+
|
|
243
|
+
instance_id: int
|
|
244
|
+
label: Optional[int] = None
|
|
245
|
+
voxel_count: int
|
|
246
|
+
centroid: tuple[float, float, float]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class InstancesResponse(BaseModel):
|
|
250
|
+
"""The instances of an instance or panoptic segmentation."""
|
|
251
|
+
|
|
252
|
+
segmentation_type: SegmentationType
|
|
253
|
+
level: int
|
|
254
|
+
voxel_size: float
|
|
255
|
+
instances: list[InstanceResponse]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# --- Filaments ---
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class FilamentCurveModel(BaseModel):
|
|
262
|
+
"""An editable or fitted filament curve (copick ``CopickFilamentCurve``); ``points`` are regenerated from it."""
|
|
263
|
+
|
|
264
|
+
kind: str
|
|
265
|
+
control_points: list[tuple[float, float, float]]
|
|
266
|
+
step: float
|
|
267
|
+
alpha: Optional[float] = None
|
|
268
|
+
degree: Optional[int] = None
|
|
269
|
+
knots: Optional[list[float]] = None
|
|
270
|
+
smoothing: Optional[float] = None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class FilamentResponse(BaseModel):
|
|
274
|
+
"""One traced filament: its ordered centreline in Angstrom."""
|
|
275
|
+
|
|
276
|
+
instance_id: int
|
|
277
|
+
points: list[tuple[float, float, float]]
|
|
278
|
+
polarity_known: bool = False
|
|
279
|
+
score: float = 1.0
|
|
280
|
+
radius: Optional[float] = None
|
|
281
|
+
curve_kind: Optional[str] = None
|
|
282
|
+
#: The stored curve, if any (it may be stale: editors check it against ``points``).
|
|
283
|
+
curve: Optional[FilamentCurveModel] = None
|
|
284
|
+
metadata: dict[str, Any] = {}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class FilamentsSummaryResponse(BaseModel):
|
|
288
|
+
"""Summary of a set of filaments (one object, user and session)."""
|
|
289
|
+
|
|
290
|
+
object_name: str
|
|
291
|
+
user_id: str
|
|
292
|
+
session_id: str
|
|
293
|
+
filament_count: int
|
|
294
|
+
color: tuple[int, int, int, int]
|
|
295
|
+
instance_ids: list[int] = []
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class FilamentsDetailResponse(BaseModel):
|
|
299
|
+
"""A set of filaments with their centrelines."""
|
|
300
|
+
|
|
301
|
+
object_name: str
|
|
302
|
+
user_id: str
|
|
303
|
+
session_id: str
|
|
304
|
+
color: tuple[int, int, int, int]
|
|
305
|
+
filaments: list[FilamentResponse]
|
|
306
|
+
voxel_spacing: Optional[float] = None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class FilamentWriteModel(BaseModel):
|
|
310
|
+
"""One filament to store: a ``curve`` (its points are regenerated by copick) or, for filaments without one,
|
|
311
|
+
the ``points`` as they are."""
|
|
312
|
+
|
|
313
|
+
instance_id: int
|
|
314
|
+
curve: Optional[FilamentCurveModel] = None
|
|
315
|
+
points: Optional[list[tuple[float, float, float]]] = None
|
|
316
|
+
polarity_known: bool = False
|
|
317
|
+
score: float = 1.0
|
|
318
|
+
radius: Optional[float] = None
|
|
319
|
+
metadata: dict[str, Any] = {}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class SaveFilamentsRequest(BaseModel):
|
|
323
|
+
"""Replace a filament set (created if missing). With ``pick_spacing`` (Angstrom), picks sampled along the
|
|
324
|
+
filaments replace the picks set of the same object, user and session."""
|
|
325
|
+
|
|
326
|
+
filaments: list[FilamentWriteModel]
|
|
327
|
+
voxel_spacing: Optional[float] = None
|
|
328
|
+
pick_spacing: Optional[float] = None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class SaveFilamentsResponse(BaseModel):
|
|
332
|
+
filaments: FilamentsDetailResponse
|
|
333
|
+
n_picks: int = 0
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# --- Request models for picks mutations ---
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class CreatePicksRequest(BaseModel):
|
|
340
|
+
"""Request to create a new picks collection."""
|
|
341
|
+
|
|
342
|
+
object_name: str
|
|
343
|
+
user_id: str
|
|
344
|
+
session_id: str
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class PointRequest(BaseModel):
|
|
348
|
+
"""Point for create/update operations."""
|
|
349
|
+
|
|
350
|
+
x: float
|
|
351
|
+
y: float
|
|
352
|
+
z: float
|
|
353
|
+
instance_id: Optional[int] = None
|
|
354
|
+
score: Optional[float] = 1.0
|
|
355
|
+
transformation: Optional[list[list[float]]] = None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class UpdatePicksRequest(BaseModel):
|
|
359
|
+
"""Request to update picks points."""
|
|
360
|
+
|
|
361
|
+
points: list[PointRequest]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class CreatePicksResponse(BaseModel):
|
|
365
|
+
"""Response after creating picks."""
|
|
366
|
+
|
|
367
|
+
object_name: str
|
|
368
|
+
user_id: str
|
|
369
|
+
session_id: str
|
|
370
|
+
color: tuple[int, int, int, int]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# API routes
|