copick-web 0.1.5__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.
- copick_web-0.1.5/.dockerignore +11 -0
- copick_web-0.1.5/.gitignore +211 -0
- copick_web-0.1.5/CHANGELOG.md +53 -0
- copick_web-0.1.5/Dockerfile +6 -0
- copick_web-0.1.5/Dockerfile.dev +11 -0
- copick_web-0.1.5/PKG-INFO +19 -0
- copick_web-0.1.5/pyproject.toml +62 -0
- copick_web-0.1.5/src/copick_web/__init__.py +3 -0
- copick_web-0.1.5/src/copick_web/app/__init__.py +1 -0
- copick_web-0.1.5/src/copick_web/app/config.py +52 -0
- copick_web-0.1.5/src/copick_web/app/dependencies.py +38 -0
- copick_web-0.1.5/src/copick_web/app/main.py +180 -0
- copick_web-0.1.5/src/copick_web/app/models.py +370 -0
- copick_web-0.1.5/src/copick_web/app/routes/__init__.py +1 -0
- copick_web-0.1.5/src/copick_web/app/routes/config.py +162 -0
- copick_web-0.1.5/src/copick_web/app/routes/filaments.py +169 -0
- copick_web-0.1.5/src/copick_web/app/routes/projects.py +15 -0
- copick_web-0.1.5/src/copick_web/app/routes/runs.py +392 -0
- copick_web-0.1.5/src/copick_web/app/routes/zarr_proxy.py +190 -0
- copick_web-0.1.5/src/copick_web/app/services/__init__.py +1 -0
- copick_web-0.1.5/src/copick_web/app/services/cache.py +103 -0
- copick_web-0.1.5/src/copick_web/app/services/compat.py +147 -0
- copick_web-0.1.5/src/copick_web/app/services/config_file.py +133 -0
- copick_web-0.1.5/src/copick_web/app/services/copick_service.py +757 -0
- copick_web-0.1.5/src/copick_web/app/services/filament_picks.py +113 -0
- copick_web-0.1.5/src/copick_web/app/services/object_types.py +245 -0
- copick_web-0.1.5/src/copick_web/app/services/project_registry.py +465 -0
- copick_web-0.1.5/src/copick_web/app/services/registry_client.py +93 -0
- copick_web-0.1.5/src/copick_web/app/services/run_info.py +148 -0
- copick_web-0.1.5/src/copick_web/app/services/thumbnails.py +132 -0
- copick_web-0.1.5/src/copick_web/app/services/zarr_urls.py +47 -0
- copick_web-0.1.5/src/copick_web/app/validation.py +46 -0
- copick_web-0.1.5/src/copick_web/cli.py +100 -0
- copick_web-0.1.5/src/copick_web/static/assets/blosc-D1xNXZJs-CRrdIaHK.js +17 -0
- copick_web-0.1.5/src/copick_web/static/assets/chunk-INHXZS53-DiyuLb3Z-DAOLzMPX.js +1 -0
- copick_web-0.1.5/src/copick_web/static/assets/index-CFHbVtPG.css +2 -0
- copick_web-0.1.5/src/copick_web/static/assets/index-DGooRI34.js +4333 -0
- copick_web-0.1.5/src/copick_web/static/assets/lz4-1Ws5oVWR-TBpHcg4u.js +15 -0
- copick_web-0.1.5/src/copick_web/static/assets/zstd-C4EcZnjq-_uEBMK3Z.js +15 -0
- copick_web-0.1.5/src/copick_web/static/copick-logo.png +0 -0
- copick_web-0.1.5/src/copick_web/static/index.html +33 -0
- copick_web-0.1.5/tests/__init__.py +0 -0
- copick_web-0.1.5/tests/conftest.py +66 -0
- copick_web-0.1.5/tests/fixtures/__init__.py +0 -0
- copick_web-0.1.5/tests/fixtures/make_demo_project.py +133 -0
- copick_web-0.1.5/tests/test_api.py +276 -0
- copick_web-0.1.5/tests/test_base_path.py +61 -0
- copick_web-0.1.5/tests/test_cache.py +59 -0
- copick_web-0.1.5/tests/test_compat.py +48 -0
- copick_web-0.1.5/tests/test_filament_edit.py +106 -0
- copick_web-0.1.5/tests/test_object_types.py +154 -0
- copick_web-0.1.5/tests/test_registry.py +188 -0
- copick_web-0.1.5/tests/test_reload.py +63 -0
- copick_web-0.1.5/tests/test_thumbnails.py +63 -0
- copick_web-0.1.5/uv.lock +3507 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Python
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
build/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
dist/
|
|
18
|
+
downloads/
|
|
19
|
+
eggs/
|
|
20
|
+
.eggs/
|
|
21
|
+
lib/
|
|
22
|
+
lib64/
|
|
23
|
+
parts/
|
|
24
|
+
sdist/
|
|
25
|
+
var/
|
|
26
|
+
wheels/
|
|
27
|
+
share/python-wheels/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
MANIFEST
|
|
32
|
+
|
|
33
|
+
# PyInstaller
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
cover/
|
|
55
|
+
|
|
56
|
+
# Translations
|
|
57
|
+
*.mo
|
|
58
|
+
*.pot
|
|
59
|
+
|
|
60
|
+
# Sphinx documentation
|
|
61
|
+
docs/_build/
|
|
62
|
+
|
|
63
|
+
# PyBuilder
|
|
64
|
+
.pybuilder/
|
|
65
|
+
target/
|
|
66
|
+
|
|
67
|
+
# Jupyter Notebook
|
|
68
|
+
.ipynb_checkpoints
|
|
69
|
+
|
|
70
|
+
# IPython
|
|
71
|
+
profile_default/
|
|
72
|
+
ipython_config.py
|
|
73
|
+
|
|
74
|
+
# Environments
|
|
75
|
+
.env*
|
|
76
|
+
.venv
|
|
77
|
+
env/
|
|
78
|
+
venv/
|
|
79
|
+
ENV/
|
|
80
|
+
env.bak/
|
|
81
|
+
venv.bak/
|
|
82
|
+
|
|
83
|
+
# mypy
|
|
84
|
+
.mypy_cache/
|
|
85
|
+
.dmypy.json
|
|
86
|
+
dmypy.json
|
|
87
|
+
|
|
88
|
+
# Pyre type checker
|
|
89
|
+
.pyre/
|
|
90
|
+
|
|
91
|
+
# pytype static type analyzer
|
|
92
|
+
.pytype/
|
|
93
|
+
|
|
94
|
+
# Cython debug symbols
|
|
95
|
+
cython_debug/
|
|
96
|
+
|
|
97
|
+
# =============================================================================
|
|
98
|
+
# Node.js / JavaScript / TypeScript
|
|
99
|
+
# =============================================================================
|
|
100
|
+
|
|
101
|
+
# Dependencies
|
|
102
|
+
node_modules/
|
|
103
|
+
.pnp
|
|
104
|
+
.pnp.js
|
|
105
|
+
|
|
106
|
+
# Build outputs
|
|
107
|
+
dist/
|
|
108
|
+
*.tsbuildinfo
|
|
109
|
+
|
|
110
|
+
# Testing
|
|
111
|
+
coverage/
|
|
112
|
+
|
|
113
|
+
# Logs
|
|
114
|
+
npm-debug.log*
|
|
115
|
+
yarn-debug.log*
|
|
116
|
+
yarn-error.log*
|
|
117
|
+
lerna-debug.log*
|
|
118
|
+
.pnpm-debug.log*
|
|
119
|
+
|
|
120
|
+
# Runtime data
|
|
121
|
+
pids
|
|
122
|
+
*.pid
|
|
123
|
+
*.seed
|
|
124
|
+
*.pid.lock
|
|
125
|
+
|
|
126
|
+
# Optional npm cache directory
|
|
127
|
+
.npm
|
|
128
|
+
|
|
129
|
+
# Optional eslint cache
|
|
130
|
+
.eslintcache
|
|
131
|
+
|
|
132
|
+
# Optional stylelint cache
|
|
133
|
+
.stylelintcache
|
|
134
|
+
|
|
135
|
+
# Microbundle cache
|
|
136
|
+
.rpt2_cache/
|
|
137
|
+
.rts2_cache_cjs/
|
|
138
|
+
.rts2_cache_es/
|
|
139
|
+
.rts2_cache_umd/
|
|
140
|
+
|
|
141
|
+
# Optional REPL history
|
|
142
|
+
.node_repl_history
|
|
143
|
+
|
|
144
|
+
# Yarn v2
|
|
145
|
+
.yarn/cache
|
|
146
|
+
.yarn/unplugged
|
|
147
|
+
.yarn/build-state.yml
|
|
148
|
+
.yarn/install-state.gz
|
|
149
|
+
.pnp.*
|
|
150
|
+
|
|
151
|
+
# Vite
|
|
152
|
+
*.local
|
|
153
|
+
|
|
154
|
+
# =============================================================================
|
|
155
|
+
# IDE / Editor
|
|
156
|
+
# =============================================================================
|
|
157
|
+
|
|
158
|
+
# JetBrains IDEs
|
|
159
|
+
.idea/
|
|
160
|
+
|
|
161
|
+
# VS Code (ignore all except shared config)
|
|
162
|
+
.vscode/*
|
|
163
|
+
!.vscode/tasks.json
|
|
164
|
+
!.vscode/extensions.json
|
|
165
|
+
*.code-workspace
|
|
166
|
+
|
|
167
|
+
# WIP Scratch folder
|
|
168
|
+
.scratch/
|
|
169
|
+
|
|
170
|
+
# Claude
|
|
171
|
+
.claude/
|
|
172
|
+
CLAUDE.md
|
|
173
|
+
|
|
174
|
+
# Vim
|
|
175
|
+
*.swp
|
|
176
|
+
*.swo
|
|
177
|
+
*~
|
|
178
|
+
|
|
179
|
+
# =============================================================================
|
|
180
|
+
# OS
|
|
181
|
+
# =============================================================================
|
|
182
|
+
|
|
183
|
+
# macOS
|
|
184
|
+
.DS_Store
|
|
185
|
+
.AppleDouble
|
|
186
|
+
.LSOverride
|
|
187
|
+
._*
|
|
188
|
+
|
|
189
|
+
# Windows
|
|
190
|
+
Thumbs.db
|
|
191
|
+
ehthumbs.db
|
|
192
|
+
Desktop.ini
|
|
193
|
+
|
|
194
|
+
# Linux
|
|
195
|
+
*~
|
|
196
|
+
|
|
197
|
+
# =============================================================================
|
|
198
|
+
# Project-specific
|
|
199
|
+
# =============================================================================
|
|
200
|
+
|
|
201
|
+
# Built client files in server (generated by npm run build:server)
|
|
202
|
+
# Keep .gitkeep but ignore actual build output
|
|
203
|
+
server/src/copick_web/static/*
|
|
204
|
+
!server/src/copick_web/static/.gitkeep
|
|
205
|
+
|
|
206
|
+
# Local config files
|
|
207
|
+
*.local.json
|
|
208
|
+
copick_config.json
|
|
209
|
+
|
|
210
|
+
# Test data
|
|
211
|
+
.test/
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.5](https://github.com/copick/copick-web/compare/server-v0.1.4...server-v0.1.5) (2026-10-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* publish copick-web to PyPI with the web UI bundled ([#39](https://github.com/copick/copick-web/issues/39)) ([6323e7c](https://github.com/copick/copick-web/commit/6323e7c7873385f76d3302e51aa78e7f265111e6))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* require Python 3.11 for the server, so Dependabot can update uv.lock ([#69](https://github.com/copick/copick-web/issues/69)) ([a150289](https://github.com/copick/copick-web/commit/a150289cc03b4c5259cce438cba828919f9dbda0))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Continuous Integration
|
|
17
|
+
|
|
18
|
+
* set up Dependabot, with a server uv.lock ([#43](https://github.com/copick/copick-web/issues/43)) ([0f0dc7d](https://github.com/copick/copick-web/commit/0f0dc7d68b6c9500c0f3c93217c600957ce5839f))
|
|
19
|
+
|
|
20
|
+
## [0.1.4](https://github.com/copick/copick-web/compare/server-v0.1.3...server-v0.1.4) (2026-10-14)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* multi-tenant with embrella integration ([#10](https://github.com/copick/copick-web/issues/10)) ([5cbd935](https://github.com/copick/copick-web/commit/5cbd935dbd6002fa98936989e0542088c8042c41))
|
|
26
|
+
|
|
27
|
+
## [0.1.3](https://github.com/copick/copick-web/compare/server-v0.1.2...server-v0.1.3) (2026-10-08)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* multi-panel viewer, filaments, instance and panoptic segmentations ([#20](https://github.com/copick/copick-web/issues/20)) ([ff48363](https://github.com/copick/copick-web/commit/ff48363349f6b523801bd5c19866ab90de189873))
|
|
33
|
+
* reload the project from the sidebar ([#22](https://github.com/copick/copick-web/issues/22)) ([05d043d](https://github.com/copick/copick-web/commit/05d043d08db674e807895311249ec78b102380c5))
|
|
34
|
+
* serve under any URL prefix, set at runtime ([#26](https://github.com/copick/copick-web/issues/26)) ([bb77a09](https://github.com/copick/copick-web/commit/bb77a09d631562e2e1a98c6ce3bddfbec30d7ef9))
|
|
35
|
+
|
|
36
|
+
## [0.1.2](https://github.com/copick/copick-web/compare/server-v0.1.1...server-v0.1.2) (2026-07-24)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
* containerize app ([#4](https://github.com/copick/copick-web/issues/4)) ([1b57cb4](https://github.com/copick/copick-web/commit/1b57cb40e51ce6553f47ea896802725e31e850bc))
|
|
42
|
+
|
|
43
|
+
## [0.1.1](https://github.com/copick/copick-web/compare/server-v0.1.0...server-v0.1.1) (2026-02-25)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Features
|
|
47
|
+
|
|
48
|
+
* Particle picking! ([26113f8](https://github.com/copick/copick-web/commit/26113f8257592c57df5f01444e4b6064c2f0ae13))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Bug Fixes
|
|
52
|
+
|
|
53
|
+
* add missing files for particle picking. ([821f7aa](https://github.com/copick/copick-web/commit/821f7aa6f43cfea4aa2e7db111d5ff5098b76ff9))
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev && rm -rf /var/lib/apt/lists/*
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN pip install --no-cache-dir .
|
|
6
|
+
CMD ["uvicorn", "copick_web.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev && rm -rf /var/lib/apt/lists/*
|
|
4
|
+
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
COPY pyproject.toml .
|
|
8
|
+
RUN mkdir -p src/copick_web && touch src/copick_web/__init__.py && \
|
|
9
|
+
pip install --no-cache-dir -e ".[dev]"
|
|
10
|
+
|
|
11
|
+
CMD ["sh", "-c", "pip install -e '.[dev]' && uvicorn copick_web.app.main:app --reload --host 0.0.0.0 --port 8000"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: copick-web
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Web visualization for copick datasets
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: click>=8.1.8
|
|
7
|
+
Requires-Dist: copick>=0.8.0
|
|
8
|
+
Requires-Dist: fastapi>=0.109.0
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
11
|
+
Requires-Dist: pydantic>=2.5.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
13
|
+
Requires-Dist: sshfs>=2024.0.0
|
|
14
|
+
Requires-Dist: uvicorn>=0.25.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: black>=25.1.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pre-commit>=4.0.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff>=0.12.0; extra == 'dev'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "copick-web"
|
|
7
|
+
version = "0.1.5"
|
|
8
|
+
description = "Web visualization for copick datasets"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi>=0.109.0",
|
|
12
|
+
"uvicorn>=0.25.0",
|
|
13
|
+
"pydantic>=2.5.0",
|
|
14
|
+
"pydantic-settings>=2.0.0",
|
|
15
|
+
"copick>=0.8.0",
|
|
16
|
+
"python-dotenv>=1.0.0",
|
|
17
|
+
"click>=8.1.8",
|
|
18
|
+
"httpx>=0.27.0",
|
|
19
|
+
"sshfs>=2024.0.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = [
|
|
24
|
+
"black>=25.1.0",
|
|
25
|
+
"ruff>=0.12.0",
|
|
26
|
+
"pytest>=8.0.0",
|
|
27
|
+
"pre-commit>=4.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
copick-web = "copick_web.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build]
|
|
34
|
+
# The client build is gitignored; ship it anyway when present (`npm run build:server` in client/).
|
|
35
|
+
artifacts = ["src/copick_web/static"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/copick_web"]
|
|
39
|
+
|
|
40
|
+
[tool.black]
|
|
41
|
+
line-length = 120
|
|
42
|
+
target-version = ['py311']
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 120
|
|
46
|
+
target-version = "py311"
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "W", "F", "I", "B", "C4"]
|
|
50
|
+
ignore = ["E501"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
53
|
+
# FastAPI's dependency-injection idiom is a call in the argument default.
|
|
54
|
+
# B008 flags every route signature otherwise.
|
|
55
|
+
extend-immutable-calls = [
|
|
56
|
+
"fastapi.Depends",
|
|
57
|
+
"fastapi.Query",
|
|
58
|
+
"fastapi.Path",
|
|
59
|
+
"fastapi.Body",
|
|
60
|
+
"fastapi.Header",
|
|
61
|
+
"fastapi.Form",
|
|
62
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# copick-web server app
|
|
@@ -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
|
|
@@ -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
|
+
)
|