ethoscopy 2.2.0__tar.gz → 2.3.0__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.
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.github/workflows/release.yml +0 -12
- ethoscopy-2.3.0/.gitignore +24 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/CLAUDE.md +41 -27
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.dummy.example +4 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.github.example +4 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.google.example +4 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.keycloak.example +4 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/Dockerfile +10 -2
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/README_AUTH.md +28 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/config/jupyterhub_config.py +39 -5
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/docker-compose.yml +11 -1
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/PKG-INFO +3 -2
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/pyproject.toml +5 -1
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/__init__.py +6 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/load.py +766 -111
- ethoscopy-2.3.0/tasks/todo.md +269 -0
- ethoscopy-2.3.0/tests/test_load_diagnostics.py +200 -0
- ethoscopy-2.3.0/tests/test_load_light.py +420 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load_optimizations.py +8 -2
- ethoscopy-2.3.0/tests/test_load_wal.py +493 -0
- ethoscopy-2.2.0/.gitignore +0 -11
- ethoscopy-2.2.0/Docker/.env.keycloak +0 -39
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.codecov.yml +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.github/workflows/ci.yml +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.pre-commit-config.yaml +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.gitlab.example +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/README.md +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/config/users.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/install_r_packages.r +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/jupyterhub_data/jupyterhub_config.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/LICENSE +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/README.md +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/TESTING.md +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/_config.yml +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/pytest.ini +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/run_tests.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/README.md +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/convert_databases.sh +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/convert_wal_to_delete.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/publish_tutorials.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/setup.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/analyse.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_HMM_class.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_class.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_core.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_draw.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_periodogram_class.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_plotly.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_seaborn.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/metadata_db.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/__init__.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/circadian_bars.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/general_functions.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/get_HMM.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/get_tutorials.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/hmm_functions.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/periodogram_functions.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/validate_datetime.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/__init__.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/conftest.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/data/README.md +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/data/test_ethoscope.db +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_analyse.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_baseline_enhancements.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_behavpy.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_behavpy_core_simple.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_compatibility_classes.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_general_functions.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_get_tutorials.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load_comprehensive.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load_metadata_fixes.py +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/1_Overview_tutorial.ipynb +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/2_HMM_tutorial.ipynb +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/3_Circadian_tutorial.ipynb +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/4_Navigating_db_tutorial.ipynb +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/5_Ethoscopy_catch22_tutorial.ipynb +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/6_Ethoscopy_to_hctsa_tutorial.ipynb +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/ethoscope_db.csv +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/jones_et_al_metadata.csv +0 -0
- {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/notebook_paper.ipynb +0 -0
|
@@ -3,9 +3,6 @@ name: Release to PyPI
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
-
push:
|
|
7
|
-
tags:
|
|
8
|
-
- 'v*.*.*'
|
|
9
6
|
|
|
10
7
|
jobs:
|
|
11
8
|
test:
|
|
@@ -58,16 +55,7 @@ jobs:
|
|
|
58
55
|
run: |
|
|
59
56
|
twine check dist/*
|
|
60
57
|
|
|
61
|
-
- name: Publish to Test PyPI
|
|
62
|
-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
|
63
|
-
env:
|
|
64
|
-
TWINE_USERNAME: __token__
|
|
65
|
-
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
|
66
|
-
run: |
|
|
67
|
-
twine upload --repository testpypi dist/*
|
|
68
|
-
|
|
69
58
|
- name: Publish to PyPI
|
|
70
|
-
if: github.event_name == 'release'
|
|
71
59
|
env:
|
|
72
60
|
TWINE_USERNAME: __token__
|
|
73
61
|
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
*.txt
|
|
2
|
+
*.pyc
|
|
3
|
+
*venv
|
|
4
|
+
dist/
|
|
5
|
+
*vscode
|
|
6
|
+
__pycache__/
|
|
7
|
+
ethoscopy.egg-info/
|
|
8
|
+
.gitignore
|
|
9
|
+
*.pkl
|
|
10
|
+
.python-version
|
|
11
|
+
tmp
|
|
12
|
+
|
|
13
|
+
# JupyterHub runtime state, generated when the Docker stack first runs.
|
|
14
|
+
# jupyterhub_cookie_secret signs session cookies and jupyterhub.sqlite holds
|
|
15
|
+
# user records and API tokens; neither belongs in the repo. Committing the
|
|
16
|
+
# secret would be the same failure as the tracked .env.keycloak removed in
|
|
17
|
+
# 6cbcbbe. jupyterhub_config.py stays tracked - it is configuration, not state.
|
|
18
|
+
Docker/jupyterhub_data/jupyterhub_cookie_secret
|
|
19
|
+
Docker/jupyterhub_data/jupyterhub.sqlite
|
|
20
|
+
|
|
21
|
+
# Coverage output
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
coverage.xml
|
|
@@ -36,48 +36,62 @@ pip install -e ".[dev]"
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
### Testing
|
|
39
|
-
- **
|
|
40
|
-
-
|
|
41
|
-
|
|
39
|
+
- **Pytest suite in `tests/`**: `python -m pytest tests/ -q` (~230 tests, seconds to run)
|
|
40
|
+
- **Caveat**: `pytest.ini` declares its section as `[tool:pytest]`, which is the
|
|
41
|
+
*setup.cfg* spelling — pytest silently ignores the file, so `addopts` (coverage,
|
|
42
|
+
`--strict-markers`) and the `markers` registrations do **not** apply. Renaming the
|
|
43
|
+
section to `[pytest]` would switch on `--cov-fail-under=70` and `--strict-markers`
|
|
44
|
+
for the whole suite; check it passes before doing so.
|
|
45
|
+
- Tutorial notebooks in `tutorial_notebook/` complement the suite for end-to-end checks
|
|
42
46
|
|
|
43
47
|
### Docker Environment
|
|
44
48
|
- **Docker available** for JupyterHub deployment
|
|
45
49
|
- Build: `JUPYTER_HUB_TAG=5.3.0 ETHOSCOPE_LAB_TAG=1.0 docker compose build`
|
|
46
50
|
- Run: `docker compose up -d` (from Docker/ directory)
|
|
47
51
|
|
|
48
|
-
###
|
|
52
|
+
### Opening databases: WAL mode and read-only mounts
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
Ethoscopes record in **WAL** (Write-Ahead Logging) mode and the results tree is
|
|
55
|
+
normally mounted read-only (`:ro` in Docker). That combination has broken loading
|
|
56
|
+
repeatedly — as `database disk image is malformed`, and as
|
|
57
|
+
`unable to open database file` on `SELECT * FROM ROI_MAP`.
|
|
51
58
|
|
|
52
|
-
**
|
|
59
|
+
**How `_connect_db()` handles it** (load.py)
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
python3 scripts/convert_wal_to_delete.py /mnt/ethoscope_data/results --verbose
|
|
61
|
+
ethoscopy never writes to these files, so the connection is *always* read-only —
|
|
62
|
+
this also stops a load from checkpointing or truncating raw data as a side effect.
|
|
63
|
+
It walks an ordered ladder of open modes, `_READ_STRATEGIES`, and **probes each
|
|
64
|
+
one with a real statement** before returning it:
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
| rung | reads | notes |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `mode=ro` | every recoverable state, incl. WAL with `-shm`, corrupt `-shm`, hot rollback journal | the only mode that sees data still in an uncheckpointed `-wal` |
|
|
69
|
+
| `immutable=1` | WAL database whose sidecars are absent on a read-only mount | ignores the `-wal`; warns via `_warn_if_wal_ignored()` if it would hide committed rows |
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
- The `_connect_db()` function in `load.py` now detects WAL mode automatically
|
|
66
|
-
- Uses `mode=ro&nolock=1` URI parameters for WAL databases on read-only mounts
|
|
67
|
-
- Includes retry logic in `read_single_roi_optimized()` for resilience
|
|
71
|
+
Two traps worth not rediscovering:
|
|
68
72
|
|
|
69
|
-
**
|
|
70
|
-
-
|
|
71
|
-
|
|
73
|
+
- **Never use `mode=ro&nolock=1`.** SQLite rejects it for *every* WAL database.
|
|
74
|
+
- **`sqlite3.connect()` never touches the file**, so a bad URI opens "successfully"
|
|
75
|
+
and only fails on the first query — past any `try/except` around the connect
|
|
76
|
+
call. That is why every rung is probed, and it is how the `nolock=1` bug survived
|
|
77
|
+
a fallback that looked like it covered the case.
|
|
78
|
+
- `immutable=1` will also "open" a **missing** path, *creating* an empty database
|
|
79
|
+
and reporting `no such table: ROI_MAP`. `_connect_db()` checks the file exists first.
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
`read_single_roi_optimized()` retries a failed read with `_connect_db(path,
|
|
82
|
+
degraded=True)`, which skips to the last rung — a retry with the default ladder
|
|
83
|
+
would just pick the same failing mode again.
|
|
84
|
+
|
|
85
|
+
**Optional**: convert databases to DELETE mode to avoid the question entirely
|
|
86
|
+
```bash
|
|
87
|
+
python3 scripts/convert_wal_to_delete.py /mnt/ethoscope_data/results --verbose
|
|
88
|
+
./scripts/convert_databases.sh /mnt/ethoscope_data/results # bash wrapper
|
|
79
89
|
```
|
|
80
90
|
|
|
91
|
+
**Tests**: `tests/test_load_wal.py` builds real SQLite files in each on-disk state
|
|
92
|
+
(read-only and writable directories) rather than mocking sqlite3 — the failures
|
|
93
|
+
being guarded against come from SQLite itself and only appear when a statement runs.
|
|
94
|
+
|
|
81
95
|
**See Also**: `Docker/README.md` for detailed database preparation instructions
|
|
82
96
|
|
|
83
97
|
## Architecture and Code Structure
|
|
@@ -16,6 +16,10 @@ ALLOWED_USERS=amadabhushi,ggilestro,mjoyce,lguo,labguest1,labguest2,labguest3,la
|
|
|
16
16
|
# Comma-separated list of admin users
|
|
17
17
|
ADMIN_USERS=ggilestro
|
|
18
18
|
|
|
19
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
20
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
21
|
+
SUPERUSERS=ggilestro
|
|
22
|
+
|
|
19
23
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
20
24
|
UID=1000
|
|
21
25
|
GID=1000
|
|
@@ -30,6 +30,10 @@ ALLOWED_USERS=username1,username2,username3
|
|
|
30
30
|
# Comma-separated list of admin users (GitHub usernames)
|
|
31
31
|
ADMIN_USERS=your-github-username
|
|
32
32
|
|
|
33
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
34
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
35
|
+
SUPERUSERS=your-github-username
|
|
36
|
+
|
|
33
37
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
34
38
|
UID=1000
|
|
35
39
|
GID=1000
|
|
@@ -32,6 +32,10 @@ ALLOWED_USERS=user1,user2,user3
|
|
|
32
32
|
# Comma-separated list of admin users
|
|
33
33
|
ADMIN_USERS=your-email-username
|
|
34
34
|
|
|
35
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
36
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
37
|
+
SUPERUSERS=your-email-username
|
|
38
|
+
|
|
35
39
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
36
40
|
UID=1000
|
|
37
41
|
GID=1000
|
|
@@ -44,6 +44,10 @@ ALLOWED_USERS=amadabhushi,ggilestro,mjoyce,lguo,labguest1,labguest2,labguest3,la
|
|
|
44
44
|
# Comma-separated list of admin users
|
|
45
45
|
ADMIN_USERS=ggilestro
|
|
46
46
|
|
|
47
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
48
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
49
|
+
SUPERUSERS=ggilestro
|
|
50
|
+
|
|
47
51
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
48
52
|
UID=1000
|
|
49
53
|
GID=1000
|
|
@@ -66,7 +66,7 @@ RUN ln -s /bin/tar /bin/gtar && \
|
|
|
66
66
|
# JUPYTER INSTALLATION AND CONFIGURATION
|
|
67
67
|
# ============================================================================
|
|
68
68
|
|
|
69
|
-
# Install JupyterLab (
|
|
69
|
+
# Install JupyterLab (latest 4.x; pip resolves the newest compatible release)
|
|
70
70
|
RUN pip3 install jupyterlab --ignore-installed
|
|
71
71
|
|
|
72
72
|
# To upgrade to version 4.0.2 (commented out)
|
|
@@ -75,6 +75,14 @@ RUN pip3 install jupyterlab --ignore-installed
|
|
|
75
75
|
# Install Jupyter extensions (compatible with 3.x only)
|
|
76
76
|
RUN pip3 install jupyterlab-spreadsheet-editor
|
|
77
77
|
|
|
78
|
+
# AI assistant: Copilot-style inline completions + chat panel.
|
|
79
|
+
# Reason: lightweight (declares only jupyter-server as its Jupyter dep), so it
|
|
80
|
+
# does NOT pull in jupyter-collaboration / server-documents like jupyter-ai does
|
|
81
|
+
# — that RTC stack is what caused "File ID could not be retrieved" on file open.
|
|
82
|
+
# Pinned for reproducible builds. Supports OpenRouter via its OpenAI-compatible
|
|
83
|
+
# provider (base URL https://openrouter.ai/api/v1).
|
|
84
|
+
RUN pip3 install --no-cache-dir "notebook-intelligence==5.1.0"
|
|
85
|
+
|
|
78
86
|
# ============================================================================
|
|
79
87
|
# PYTHON PACKAGES AND ETHOSCOPE SETUP
|
|
80
88
|
# ============================================================================
|
|
@@ -98,7 +106,7 @@ RUN pip3 install \
|
|
|
98
106
|
pyarrow \
|
|
99
107
|
tqdm \
|
|
100
108
|
jupyterlab-git \
|
|
101
|
-
ethoscopy==2.1
|
|
109
|
+
ethoscopy==2.2.1
|
|
102
110
|
# pycatch22
|
|
103
111
|
|
|
104
112
|
# Pre-populate tutorial datasets inside the installed package so non-root
|
|
@@ -15,6 +15,7 @@ This JupyterHub deployment supports multiple authentication methods, configurabl
|
|
|
15
15
|
- [Adding New Users (OAuth)](#adding-new-users-oauth)
|
|
16
16
|
- [Adding New Users (Dummy Auth)](#adding-new-users-dummy-auth)
|
|
17
17
|
- [Making Users Admins](#making-users-admins)
|
|
18
|
+
- [Granting Cross-User File Access (Superusers)](#granting-cross-user-file-access-superusers)
|
|
18
19
|
- [Switching Authentication Methods](#switching-authentication-methods)
|
|
19
20
|
- [Dockerfile Optimization](#dockerfile-optimization)
|
|
20
21
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -388,6 +389,33 @@ docker compose restart
|
|
|
388
389
|
- Navigate to: `https://jupyter.lab.gilest.ro/hub/admin`
|
|
389
390
|
- You should see the admin panel with all users listed
|
|
390
391
|
|
|
392
|
+
### Granting Cross-User File Access (Superusers)
|
|
393
|
+
|
|
394
|
+
By default JupyterLab roots each user's file browser at their own home (`/home/<username>`). A **superuser** instead gets `/home` as their root, so they can browse, open, and run notebooks across every user's directory directly from their own JupyterLab session.
|
|
395
|
+
|
|
396
|
+
This works because all single-user servers in this deployment run under the same container UID (see `ConfigUserSpawner` in `config/jupyterhub_config.py`), so filesystem-level access to all homes already exists — `SUPERUSERS` simply surfaces it in the UI.
|
|
397
|
+
|
|
398
|
+
**Configuration**:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
# .env
|
|
402
|
+
SUPERUSERS=ggilestro,jdoe
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
If `SUPERUSERS` is unset, it defaults to `ADMIN_USERS`.
|
|
406
|
+
|
|
407
|
+
**Apply changes**:
|
|
408
|
+
```bash
|
|
409
|
+
docker compose restart
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Verify**:
|
|
413
|
+
- Log in as a superuser
|
|
414
|
+
- The JupyterLab file browser should list every user's home folder
|
|
415
|
+
- Opening another user's `.ipynb` runs it in place; edits and kernel output write back to that user's home
|
|
416
|
+
|
|
417
|
+
**Security note**: Regular users are not isolated from each other at the filesystem level in this setup — anyone with a JupyterLab terminal can already `cd` into another user's home. `SUPERUSERS` only changes what is shown by default in the file browser. True per-user isolation would require a different spawner (e.g. `DockerSpawner` or `KubeSpawner` with distinct UIDs).
|
|
418
|
+
|
|
391
419
|
---
|
|
392
420
|
|
|
393
421
|
## Dockerfile Optimization
|
|
@@ -120,6 +120,17 @@ else:
|
|
|
120
120
|
# Use imported list from users.py
|
|
121
121
|
c.Authenticator.admin_users = ADMIN_USERS
|
|
122
122
|
|
|
123
|
+
# Superusers: users whose JupyterLab file browser is rooted at /home instead
|
|
124
|
+
# of their own home, so they can browse/open/run notebooks across every user.
|
|
125
|
+
# Reason: all single-user servers run as the same container UID (see
|
|
126
|
+
# ConfigUserSpawner below), so filesystem access already exists — this just
|
|
127
|
+
# exposes it in the UI. Defaults to admins; override with SUPERUSERS env var.
|
|
128
|
+
superusers_env = os.getenv('SUPERUSERS')
|
|
129
|
+
if superusers_env is not None:
|
|
130
|
+
SUPERUSERS = {u.strip() for u in superusers_env.split(',') if u.strip()}
|
|
131
|
+
else:
|
|
132
|
+
SUPERUSERS = set(c.Authenticator.admin_users)
|
|
133
|
+
|
|
123
134
|
# ============================================================================
|
|
124
135
|
# CUSTOM SPAWNER (Preserved - critical for current setup)
|
|
125
136
|
# ============================================================================
|
|
@@ -129,25 +140,48 @@ class ConfigUserSpawner(LocalProcessSpawner):
|
|
|
129
140
|
def make_preexec_fn(self, name):
|
|
130
141
|
"""Don't try to switch users - run everything as current user"""
|
|
131
142
|
return None
|
|
132
|
-
|
|
143
|
+
|
|
133
144
|
def user_env(self, env):
|
|
134
145
|
"""Set user environment without system user lookup"""
|
|
135
146
|
env = env.copy()
|
|
136
147
|
home_dir = f'/home/{self.user.name}'
|
|
137
|
-
|
|
148
|
+
|
|
138
149
|
# Ensure home directory exists with proper permissions
|
|
139
150
|
os.makedirs(home_dir, mode=0o755, exist_ok=True)
|
|
140
|
-
|
|
151
|
+
|
|
141
152
|
# Set environment variables
|
|
142
153
|
env['USER'] = self.user.name
|
|
143
154
|
env['HOME'] = home_dir
|
|
144
155
|
env['SHELL'] = '/bin/bash'
|
|
145
156
|
env['LOGNAME'] = self.user.name
|
|
146
|
-
|
|
157
|
+
|
|
158
|
+
# Notebook Intelligence (AI assistant): forward central LLM config from
|
|
159
|
+
# the container environment so students never enter API keys themselves
|
|
160
|
+
# (avoids keys leaking into dotfiles, cf. the pxin OpenAI-key incident).
|
|
161
|
+
# LiteLLM reads OPENROUTER_API_KEY directly and routes the
|
|
162
|
+
# `openrouter/...` model id; values come from .env via docker-compose.
|
|
163
|
+
for _key in (
|
|
164
|
+
'OPENROUTER_API_KEY',
|
|
165
|
+
'NBI_CHAT_MODEL_PROVIDER', 'NBI_CHAT_MODEL_ID',
|
|
166
|
+
'NBI_INLINE_COMPLETION_MODEL_PROVIDER', 'NBI_INLINE_COMPLETION_MODEL_ID',
|
|
167
|
+
):
|
|
168
|
+
_val = os.getenv(_key)
|
|
169
|
+
if _val:
|
|
170
|
+
env[_key] = _val
|
|
171
|
+
|
|
147
172
|
return env
|
|
148
173
|
|
|
149
174
|
c.JupyterHub.spawner_class = ConfigUserSpawner
|
|
150
|
-
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _set_notebook_dir(spawner):
|
|
178
|
+
if spawner.user.name in SUPERUSERS:
|
|
179
|
+
spawner.notebook_dir = '/home'
|
|
180
|
+
else:
|
|
181
|
+
spawner.notebook_dir = f'/home/{spawner.user.name}'
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
c.Spawner.pre_spawn_hook = _set_notebook_dir
|
|
151
185
|
|
|
152
186
|
# Timeouts
|
|
153
187
|
c.Spawner.http_timeout = 60
|
|
@@ -6,13 +6,14 @@ services:
|
|
|
6
6
|
dockerfile: Dockerfile
|
|
7
7
|
args:
|
|
8
8
|
JUPYTER_HUB_TAG: ${JUPYTER_HUB_TAG:-5.4.0}
|
|
9
|
-
image: ggilestro/ethoscope-lab:${ETHOSCOPE_LAB_TAG:-1.
|
|
9
|
+
image: ggilestro/ethoscope-lab:${ETHOSCOPE_LAB_TAG:-1.4}
|
|
10
10
|
container_name: ethoscope-lab
|
|
11
11
|
ports:
|
|
12
12
|
- 8082:8000
|
|
13
13
|
volumes:
|
|
14
14
|
- /mnt/ethoscope_data/results:/mnt/ethoscope_results:ro
|
|
15
15
|
- /mnt/ethoscope_data/ethoscope_metadata:/opt/ethoscope_metadata
|
|
16
|
+
- /mnt/data/projects:/mnt/data/projects:ro
|
|
16
17
|
- /mnt/homes:/home
|
|
17
18
|
- /mnt/cache:/home/cache
|
|
18
19
|
- ./jupyterhub_data:/srv/jupyterhub/
|
|
@@ -34,6 +35,7 @@ services:
|
|
|
34
35
|
- DUMMY_PASSWORD=${DUMMY_PASSWORD:-ethoscope}
|
|
35
36
|
- ALLOWED_USERS=${ALLOWED_USERS}
|
|
36
37
|
- ADMIN_USERS=${ADMIN_USERS}
|
|
38
|
+
- SUPERUSERS=${SUPERUSERS}
|
|
37
39
|
# OAuth settings (only needed when using OAuth authenticators)
|
|
38
40
|
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
|
39
41
|
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
|
@@ -47,6 +49,14 @@ services:
|
|
|
47
49
|
- GITHUB_ORG=${GITHUB_ORG}
|
|
48
50
|
- GOOGLE_HOSTED_DOMAIN=${GOOGLE_HOSTED_DOMAIN}
|
|
49
51
|
- GITLAB_URL=${GITLAB_URL}
|
|
52
|
+
# Notebook Intelligence (AI assistant) — central OpenRouter config.
|
|
53
|
+
# Put OPENROUTER_API_KEY in .env (gitignored). Models are overridable;
|
|
54
|
+
# verify/adjust slugs at https://openrouter.ai/models
|
|
55
|
+
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
|
56
|
+
- NBI_CHAT_MODEL_PROVIDER=${NBI_CHAT_MODEL_PROVIDER:-litellm-compatible}
|
|
57
|
+
- NBI_CHAT_MODEL_ID=${NBI_CHAT_MODEL_ID:-openrouter/anthropic/claude-sonnet-4.5}
|
|
58
|
+
- NBI_INLINE_COMPLETION_MODEL_PROVIDER=${NBI_INLINE_COMPLETION_MODEL_PROVIDER:-litellm-compatible}
|
|
59
|
+
- NBI_INLINE_COMPLETION_MODEL_ID=${NBI_INLINE_COMPLETION_MODEL_ID:-openrouter/openai/gpt-4o-mini}
|
|
50
60
|
|
|
51
61
|
user: "${UID:-1000}:${GID:-1000}" # Adjust to your own user. Make sure this matches the permissions of your /mnt/{homes,cache} folder
|
|
52
62
|
restart: always
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ethoscopy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: "A python based toolkit to download and anlyse data from the Ethoscope hardware system."
|
|
5
5
|
Author-email: Lblackhurst29 <lblackhurst29@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -19,6 +19,7 @@ Requires-Dist: hmmlearn<0.4.0,>=0.3.2
|
|
|
19
19
|
Requires-Dist: nbformat<6.0.0,>=5.10.4
|
|
20
20
|
Requires-Dist: numpy<3.0.0,>=2.0.0
|
|
21
21
|
Requires-Dist: pandas<3.0.0,>=2.2.2
|
|
22
|
+
Requires-Dist: pillow<13.0,>=10.0
|
|
22
23
|
Requires-Dist: plotly<6.0.0,>=5.22.0
|
|
23
24
|
Requires-Dist: pywavelets<2.0.0,>=1.6.0
|
|
24
25
|
Requires-Dist: seaborn<0.14.0,>=0.13.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ethoscopy"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.0"
|
|
4
4
|
description = "\"A python based toolkit to download and anlyse data from the Ethoscope hardware system.\""
|
|
5
5
|
authors = [{name = "Lblackhurst29",email = "lblackhurst29@gmail.com"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -16,6 +16,10 @@ dependencies = [
|
|
|
16
16
|
"astropy>=7.0,<8.0",
|
|
17
17
|
"nbformat>=5.10.4,<6.0.0",
|
|
18
18
|
"tqdm>=4.66,<5.0",
|
|
19
|
+
# estimate_light_cycle() decodes the stored IMG_SNAPSHOTS JPEGs. Pillow
|
|
20
|
+
# already arrived transitively through seaborn -> matplotlib; this makes the
|
|
21
|
+
# direct dependency explicit.
|
|
22
|
+
"pillow>=10.0,<13.0",
|
|
19
23
|
]
|
|
20
24
|
requires-python = ">=3.12,<4.0"
|
|
21
25
|
classifiers = [
|
|
@@ -15,8 +15,11 @@ from ethoscopy.analyse import (
|
|
|
15
15
|
from ethoscopy.behavpy import behavpy
|
|
16
16
|
from ethoscopy.load import (
|
|
17
17
|
download_from_remote_dir,
|
|
18
|
+
estimate_light_cycle,
|
|
18
19
|
link_meta_index,
|
|
19
20
|
load_ethoscope,
|
|
21
|
+
load_ethoscope_diagnostics,
|
|
22
|
+
load_ethoscope_light_schedule,
|
|
20
23
|
load_ethoscope_metadata,
|
|
21
24
|
)
|
|
22
25
|
from ethoscopy.misc.general_functions import concat
|
|
@@ -28,9 +31,12 @@ __all__ = [
|
|
|
28
31
|
"behavpy",
|
|
29
32
|
"download_from_remote_dir",
|
|
30
33
|
"download_tutorial_data",
|
|
34
|
+
"estimate_light_cycle",
|
|
31
35
|
"get_tutorial",
|
|
32
36
|
"link_meta_index",
|
|
33
37
|
"load_ethoscope",
|
|
38
|
+
"load_ethoscope_diagnostics",
|
|
39
|
+
"load_ethoscope_light_schedule",
|
|
34
40
|
"load_ethoscope_metadata",
|
|
35
41
|
"max_velocity_detector",
|
|
36
42
|
"sleep_annotation",
|